Preview Azure Infrastructure with Bicep
Preview Azure compute and networking with Bicep What-If, without deployment.
Introduction
30 Second Summary
A pay-as-you-go cloud account can make experimentation feel risky because each live resource may carry a charge. Seeing the impact first gives you room to reason before you commit.
In this project, you will build a two-file Bicep plan for a private Linux VM. You will use Azure Resource Manager What-If to expose a subnet security gap before confirming the corrected design without deployment.
What You'll Build
When you run your finished plan, Azure previews a private Linux VM with its complete supporting infrastructure without creating anything.
By the end of this project, you'll have:
- A subscription-scope preview where you see planned Create changes while your subscription stays free of the planned resources.
- A linked infrastructure design that lets you trace the VM through its network interface, subnet, virtual network, and managed disks.
- A before-and-after security check that exposes a missing NSG association. You then confirm the workload subnet is protected in the corrected preview.
- Secret Mission: Extend the virtual network with a separate management subnet that shares the existing NSG.
Are there any prerequisites?
You need an Azure account with a pay-as-you-go subscription. Your account needs permission to run subscription-scope deployment What-If operations.
Before We Start
This checkpoint keeps your Azure architecture work focused on a zero-cost plan. You will use Azure Resource Manager What-If to preview compute, storage, and networking resources without deploying them.
Open a Cost-Safe Azure Workstation
Your Azure infrastructure plan needs an authenticated workspace before Azure can evaluate it. A local installation adds unnecessary setup before you can inspect the plan.
Persistent Azure Cloud Shell storage can introduce storage costs. An ephemeral session requires no storage account.
In this step, you will verify the Azure CLI toolchain. You will also confirm the Azure CLI-managed Bicep CLI is ready.
In this step, get ready to:
- Start an ephemeral Bash session in Azure Cloud Shell.
- Verify the Azure CLI version plus the Azure CLI-managed Bicep CLI.
- Confirm your intended pay-as-you-go subscription is current.
Launch an ephemeral Bash session
Cloud Shell gives you an authenticated terminal inside the Azure portal. You use your existing Azure account, so this step creates no new credential.
Why ephemeral Cloud Shell?
The Cloud Shell hosting machine is free. Persistent storage has regular storage costs.
The ephemeral option creates no storage account. Files from this session disappear when the session ends.
- Open the Microsoft Azure portal in your browser.
- Sign in with the Azure account connected to your pay-as-you-go subscription.
- Launch Cloud Shell from the portal toolbar.
- Choose Bash as the shell.
- Select No storage account required for an ephemeral session.
- Register Microsoft.CloudShell if a resource provider registration prompt appears.
- Reopen Cloud Shell after the registration finishes.
- Confirm the Cloud Shell terminal opens with a Bash prompt.
Your Bash prompt confirms the ephemeral workstation is open. No storage account was created for the session.
Cloud Shell does not open?
Confirm the resource provider registration finished before reopening Cloud Shell. Check that you selected Bash with the ephemeral session option.
If the terminal still does not open, help me troubleshoot my ephemeral Azure Cloud Shell session.
Verify Azure CLI and Bicep
The next project steps depend on Azure CLI 2.76.0 or later because they use the --validation-level option. Bicep runs through the CLI-managed installation in Cloud Shell.
If this session has not used Bicep before, Azure CLI can install its managed Bicep CLI during the version check.
- Check both installed versions by running:
az --version
az bicep version
What do these checks prove?
- The first command reports the Azure CLI version installed in Cloud Shell.
- The second command confirms the Azure CLI-managed Bicep CLI is available.
The output now falls into one of three states. Use the matching tab to confirm your workstation is ready.
✔️ I see the minimum version or higher
You're set. Azure CLI reports 2.76.0 or later.
Bicep also returns a version number. Both tools are ready for the infrastructure preview.
ⓧ I see an older version
This hosted session does not meet the minimum Azure CLI version for the project.
- Close the Cloud Shell pane.
- Reopen Cloud Shell from the Azure portal toolbar.
- Choose Bash again.
- Select No storage account required for an ephemeral session again.
- Repeat the two version checks from the main instructions.
- Consult the official Azure Cloud Shell tool documentation if a fresh session remains below the minimum version.
Fresh session still shows an older version?
Cloud Shell manages the Azure CLI installation. A fresh hosted session that remains below the requirement needs environment-specific support.
Ask for help to diagnose why Azure Cloud Shell has an older Azure CLI version.
ⓧ A command is unavailable
Azure CLI plus Bicep are preinstalled in Azure Cloud Shell. An unavailable command usually means the hosted terminal did not initialize correctly.
- Confirm the shell selector shows Bash.
- Close the Cloud Shell pane.
- Reopen the ephemeral Cloud Shell session.
- Repeat the two version checks from the main instructions.
Command still unavailable?
A fresh Bash session should include both tools. A repeated missing command points to a Cloud Shell environment issue.
Ask for help to restore the missing Azure CLI or Bicep command in Cloud Shell.
Confirm the current subscription
Azure CLI sends later operations to the current subscription. Confirming that selection now keeps every preview scoped to your intended pay-as-you-go subscription.
Before you run the check, which subscription do you expect Cloud Shell to mark as current?
- List the subscriptions available to your signed-in account by running:
az account list
What does this command show?
The command lists the subscriptions available to your signed-in Azure account. The isDefault field identifies the current subscription.
- Find the pay-as-you-go subscription you intend to use.
- Record its name or ID here: your subscription name or ID.
- Check whether its isDefault field is true.
✔️ My intended subscription is current
Good. Your intended pay-as-you-go subscription is already the target for later Azure CLI operations.
ⓧ Another subscription is current
The current selection controls where later preview operations run. Set the intended subscription before continuing.
- Set your intended subscription as current by running:
az account set --subscription [[AZURE_SUBSCRIPTION="your subscription name or ID"]]
What does this command change?
This command changes the active Azure CLI subscription for the session. It does not deploy or modify any Azure resources.
Before the final check, do you expect the tool versions plus the current subscription to match the project requirements?
- Run the complete workstation check with these commands:
az --version
az bicep version
az account list
What should you see?
- Azure CLI reports version 2.76.0 or later.
- Bicep returns a version number.
- Your intended pay-as-you-go subscription has isDefault set to true.
Final check does not match?
An older Azure CLI version needs a fresh Cloud Shell session. An incorrect current subscription needs the subscription selection command from the previous tab.
Ask for help to diagnose my final Azure Cloud Shell workstation check.
Your cost-safe workstation is ready. Cloud Shell can now evaluate the Azure plan without creating the planned compute, storage, or networking resources.
Your Azure toolchain is verified. Next, you will build the network and storage foundation that produces the first visible What-If preview.
Preview the Network and Storage Foundation
Your ephemeral Azure Cloud Shell session gives you an authenticated place to write the plan. A smaller foundation keeps the first preview readable before compute dependencies arrive.
In this step, you will use Bicep to describe a resource group with its network foundation. You will then use Azure Resource Manager What-If to preview the storage addition without creating resources.
In this step, get ready to:
- Create a subscription-scope entry file that delegates infrastructure planning to a module.
- Define a virtual network with a workload subnet plus a secure storage account.
- Preview the planned resources without deploying them.
Create the subscription entry point
A subscription scope template can plan the resource group itself. Its module then plans the resources inside that group.
- Open the current Cloud Shell directory in the integrated editor by running this command:
code .
What Does This Command Do?
The command opens the current Cloud Shell directory in the integrated editor. Files you create there stay available for the rest of this ephemeral session.
You should see the VS Code editor with the current Cloud Shell directory in its file explorer.
Editor Does Not Open?
Return to the Cloud Shell terminal from earlier. Confirm that the Bash prompt is active before trying the command again.
Ask for help if the integrated editor remains unavailable: help me open the integrated editor from my ephemeral Azure Cloud Shell Bash session.
- Create a file named main.bicep in the current Cloud Shell directory.
- Fill main.bicep by pasting this code:
targetScope = 'subscription'
@description('Azure region used for the planned resource group and resources.')
param location string = 'eastus'
@description('Name of the planned resource group.')
param resourceGroupName string = 'rg-az104-whatif'
resource labResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: resourceGroupName
location: location
tags: {
purpose: 'az104-whatif'
}
}
module infrastructure 'infrastructure.bicep' = {
name: 'az104InfrastructurePreview'
scope: labResourceGroup
params: {
location: location
prefix: 'az104lab'
}
}
What Does This Code Do?
- The targetScope declaration allows the plan to include the resource group.
- The location parameter places the planned group in eastus.
- The labResourceGroup resource describes rg-az104-whatif.
- The infrastructure module passes the location plus the az104lab naming prefix into infrastructure.bicep.
- Save main.bicep by pressing Ctrl+S.
- Check the editor file explorer for main.bicep.
You should see main.bicep listed in the current Cloud Shell directory.
Cannot Find main.bicep?
- Confirm that the filename is exactly main.bicep.
- Confirm that the file sits in the directory opened by the editor.
- Press Ctrl+S again to save the file.
Ask for help if the file still does not appear: help me find the main.bicep file in my Azure Cloud Shell editor.
Define the network foundation
A virtual network reserves the private address space for later resources. The workload subnet carves out a smaller range inside that network.
- Create a file named infrastructure.bicep beside main.bicep.
- Define the module parameters plus the virtual network by pasting this code into infrastructure.bicep:
@description('Azure region used for the planned resources.')
param location string
@description('Short prefix used for resource names.')
param prefix string
var storageAccountName = 'st${uniqueString(resourceGroup().id)}'
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: '${prefix}-vnet'
location: location
properties: {
addressSpace: {
addressPrefixes: [
'10.20.0.0/16'
]
}
subnets: [
{
name: 'snet-workload'
properties: {
addressPrefix: '10.20.1.0/24'
}
}
]
}
}
What Does This Code Do?
- The module receives location plus prefix from main.bicep.
- The storageAccountName variable prepares a unique name based on the resource group ID.
- The virtual network uses the address space 10.20.0.0/16.
- The snet-workload subnet uses the smaller prefix 10.20.1.0/24.
- Save infrastructure.bicep by pressing Ctrl+S.
- Return to the Cloud Shell terminal by pressing Ctrl+`.
Before you run the preview, which planned resources do you expect Azure to report from these two files?
Preview Only
What-If evaluates the template without changing resources. This keeps the pay-as-you-go subscription free of the planned lab resources.
Stop after each preview in this project. Do not switch to a deployment operation.
- Preview the network foundation by running this command:
az deployment sub what-if --template-file main.bicep --location eastus --validation-level Template
What Does This Command Do?
The command evaluates main.bicep at subscription scope. The --validation-level Template option performs static syntax plus structure validation.
This validation level skips provider availability preflight. It also skips permission preflight.
You should see planned Create changes for rg-az104-whatif plus az104lab-vnet. The virtual network payload should contain snet-workload with the prefix 10.20.1.0/24.
That first preview is a real win. Azure has evaluated your network plan while your subscription still has no deployed lab resources.
Preview Does Not Complete?
- Confirm that main.bicep sits beside infrastructure.bicep.
- Confirm that both filenames match the module declaration exactly.
- Compare the reported line with the matching code block above.
Ask for help with the reported issue: help me debug my subscription-scope Bicep What-If preview.
Add storage and inspect the complete preview
The storage account adds the storage part of the foundation. Its generated name stays within Azure naming rules while remaining unique to the planned resource group.
- Return to the integrated editor by pressing Ctrl+`.
- Place the cursor below the closing brace of the virtualNetwork resource in infrastructure.bicep.
- Add the storage account by pasting this code:
resource storageAccount 'Microsoft.Storage/storageAccounts@2025-06-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
}
}
What Does This Code Do?
- The account uses the generated value held by storageAccountName.
- The Standard_LRS SKU selects locally redundant storage.
- The StorageV2 kind creates a general-purpose storage design.
- The properties require HTTPS plus TLS1_2.
- The allowBlobPublicAccess setting blocks public blob access.
- Save infrastructure.bicep by pressing Ctrl+S.
- Return to the Cloud Shell terminal by pressing Ctrl+`.
Before you run the final check, which new planned resource should appear beside the network foundation?
- Preview the complete foundation by running the same command:
az deployment sub what-if --template-file main.bicep --location eastus --validation-level Template
What Should I See?
This second preview evaluates the saved storage addition. It still stops before deployment.
- The change summary shows planned Create changes.
- The resource group appears as rg-az104-whatif.
- The virtual network appears as az104lab-vnet.
- The virtual network payload includes snet-workload with 10.20.1.0/24.
- A uniquely named StorageV2 account appears in the plan.
- The command finishes without a deployment confirmation prompt.
You now have a validated network plus storage preview. Your Azure subscription remains free of the planned infrastructure.
Storage Missing From the Preview?
- Confirm that the storageAccount resource sits outside the virtualNetwork closing brace.
- Confirm that storageAccountName matches the variable declared near the top of infrastructure.bicep.
- Save infrastructure.bicep before repeating the preview.
Ask for help if the storage account still does not appear: help me debug why my storage account is missing from the Bicep What-If preview.
✔️ Awesome, I've got everything!
Great. Double check that both Bicep files are saved before moving on.
ⓧ I'd like to double check the full code
Compare both saved files with the exact foundation code below.
targetScope = 'subscription'
@description('Azure region used for the planned resource group and resources.')
param location string = 'eastus'
@description('Name of the planned resource group.')
param resourceGroupName string = 'rg-az104-whatif'
resource labResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: resourceGroupName
location: location
tags: {
purpose: 'az104-whatif'
}
}
module infrastructure 'infrastructure.bicep' = {
name: 'az104InfrastructurePreview'
scope: labResourceGroup
params: {
location: location
prefix: 'az104lab'
}
}
Role of main.bicep
This file owns the subscription scope plus the planned resource group. It sends resource-group infrastructure to the module.
@description('Azure region used for the planned resources.')
param location string
@description('Short prefix used for resource names.')
param prefix string
var storageAccountName = 'st${uniqueString(resourceGroup().id)}'
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: '${prefix}-vnet'
location: location
properties: {
addressSpace: {
addressPrefixes: [
'10.20.0.0/16'
]
}
subnets: [
{
name: 'snet-workload'
properties: {
addressPrefix: '10.20.1.0/24'
}
}
]
}
}
resource storageAccount 'Microsoft.Storage/storageAccounts@2025-06-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
}
}
Role of infrastructure.bicep
This module owns the virtual network plus its workload subnet. It also defines the uniquely named storage account.
Your foundation plan is proven without deployment. Next, you will extend it with private compute plus its disk relationships.
Add Compute and Expose the Security Gap
Your foundation preview proved that Azure Resource Manager What-If can evaluate networking and storage without creating resources. You can now extend that plan with private compute.
A valid Bicep compute plan can still leave a security question unanswered. This step reveals whether the workload subnet has a network control attached.
In this step, get ready to:
- Generate a temporary SSH key for the planned Linux VM.
- Extend both Bicep files with the private compute design.
- Rerun the What-If preview. Identify the missing subnet protection.
Generate a temporary SSH key
The planned VM uses SSH public-key authentication. The private key remains inside your ephemeral Azure Cloud Shell session.
The key-generation command pauses twice for a passphrase. An empty passphrase lets the later Bash input flow use this temporary key.
- Switch back to the Cloud Shell terminal by pressing Ctrl+`.
- Press Enter when the command asks for a passphrase.
- Press Enter when the command asks for the passphrase again.
- Generate the temporary RSA key pair by running this command:
ssh-keygen -m PEM -t rsa -b 4096 -f ~/.ssh/id_rsa.pem
What does this command create?
- The command stores the private key at ~/.ssh/id_rsa.pem.
- It stores the shareable public key at ~/.ssh/id_rsa.pem.pub.
- The 4096 value sets the RSA key size used for this temporary validation key.
- Display the public half of the key by running this command:
cat ~/.ssh/id_rsa.pem.pub
Which key do you use?
The line ending in .pub is the public key that the VM plan accepts. Keep the private ~/.ssh/id_rsa.pem file inside Cloud Shell.
Your temporary authentication material is ready. The public key can now validate the VM profile without exposing the private key.
Key generation stopped?
Confirm that you are still using the ephemeral Bash session from earlier. Check that the ~/.ssh directory is available in that session.
If the target path already contains a key you need, do not overwrite it. Use a fresh ephemeral Cloud Shell session for this temporary project key.
Ask for help with the exact output you see: help me diagnose why SSH key generation stopped in Azure Cloud Shell
Add private compute to the plan
The public key must travel from the subscription-scope file into the resource-group module. The module then uses it in the planned Linux authentication profile.
The network interface gives the virtual machine a private dynamic address on the existing workload subnet. No public IP enters the design.
- Return to the Cloud Shell editor by pressing Ctrl+`.
- In infrastructure.bicep, find the existing storageAccountName variable.
- Replace that variable line with this parameter and variable block:
@secure()
@description('Temporary SSH public key used only to validate the VM plan.')
param adminPublicKey string
param adminUsername string = 'az104admin'
var storageAccountName = 'st${uniqueString(resourceGroup().id)}'
var imageReference = {
publisher: 'Canonical'
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts-gen2'
version: 'latest'
}
What does this block define?
- The @secure() decorator marks adminPublicKey as sensitive input.
- The adminUsername parameter fixes the Linux administrator name as az104admin.
- The imageReference object identifies the Ubuntu image used by the planned VM.
- Save infrastructure.bicep by pressing Ctrl+S.
- Confirm that adminPublicKey appears above adminUsername in the editor.
- Confirm that imageReference appears directly below storageAccountName.
Seeing parameter errors?
Check that @secure() sits directly above the description for adminPublicKey. Confirm that each string value keeps its single quotes.
Ask for help with the highlighted line: help me fix the new parameters and image reference in infrastructure.bicep
- Switch to the existing main.bicep editor tab.
- Replace the file contents with this updated subscription-scope entry file:
targetScope = 'subscription'
@description('Azure region used for the planned resource group and resources.')
param location string = 'eastus'
@description('Name of the planned resource group.')
param resourceGroupName string = 'rg-az104-whatif'
@secure()
@description('Temporary SSH public key used only to validate the VM plan.')
param adminPublicKey string
resource labResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: resourceGroupName
location: location
tags: {
purpose: 'az104-whatif'
}
}
module infrastructure 'infrastructure.bicep' = {
name: 'az104InfrastructurePreview'
scope: labResourceGroup
params: {
location: location
prefix: 'az104lab'
adminPublicKey: adminPublicKey
}
}
How does the key reach the module?
The subscription-scope adminPublicKey parameter receives the public key from the What-If command. The module passes that value into the matching secure parameter in infrastructure.bicep.
- Save main.bicep by pressing Ctrl+S.
- Confirm that adminPublicKey appears once as a subscription parameter.
- Confirm that adminPublicKey also appears inside the module's params object.
Module parameter still highlighted?
Check that both files spell adminPublicKey with the same capitalization. Confirm that infrastructure.bicep has been saved.
Ask for help comparing the module boundary: help me debug the adminPublicKey parameter flow between my Bicep files
The compute plan needs a private attachment to the existing subnet. The network interface holds that connection for the VM.
- Switch back to infrastructure.bicep.
- Find the closing brace of the existing storageAccount resource.
- Add this network interface resource directly below it:
resource networkInterface 'Microsoft.Network/networkInterfaces@2023-09-01' = {
name: '${prefix}-nic'
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
subnet: {
id: virtualNetwork.properties.subnets[0].id
}
privateIPAllocationMethod: 'Dynamic'
}
}
]
}
}
What does the network interface do?
- The interface name resolves to az104lab-nic through the existing prefix value.
- The subnet ID points to the first subnet in virtualNetwork.
- The Dynamic allocation method requests a private address from that subnet.
- Save infrastructure.bicep.
- Confirm that networkInterface appears below storageAccount.
- Confirm that its subnet reference ends with subnets[0].id.
Network interface reference highlighted?
Check that the existing virtual network symbol is spelled virtualNetwork. Keep the index and ID reference exactly as virtualNetwork.properties.subnets[0].id.
Ask for help with the subnet reference: help me fix the private NIC resource in infrastructure.bicep
The VM resource brings together hardware sizing and managed storage. Start with those complete sections before connecting the network profile.
- Place the cursor directly below the networkInterface resource.
- Add the VM hardware and storage sections with this block:
resource virtualMachine 'Microsoft.Compute/virtualMachines@2023-09-01' = {
name: '${prefix}-vm'
location: location
properties: {
hardwareProfile: {
vmSize: 'Standard_D2s_v3'
}
storageProfile: {
imageReference: imageReference
osDisk: {
createOption: 'FromImage'
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
dataDisks: [
{
name: '${prefix}-data-01'
diskSizeGB: 1
lun: 0
createOption: 'Empty'
caching: 'ReadWrite'
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
]
}
}
}
What storage does the VM use?
- The hardwareProfile selects the Standard_D2s_v3 VM size.
- The OS disk uses the selected image with Standard_LRS managed storage.
- The data disk resolves to az104lab-data-01 with a diskSizeGB value of 1.
- Save infrastructure.bicep.
- Confirm that the new virtualMachine resource ends with two closing braces.
- Confirm that dataDisks sits inside storageProfile.
The next insertion point is a little fiddly because the VM has several nested braces. The network profile belongs inside properties immediately after storageProfile.
- Place the cursor after the closing brace for storageProfile.
- Add the network profile before the brace that closes properties with this block:
networkProfile: {
networkInterfaces: [
{
id: networkInterface.id
}
]
}
How does the VM reach the subnet?
The VM references networkInterface.id instead of defining another connection. That dependency links the VM to the private interface you just added.
- Save infrastructure.bicep.
- Confirm that networkProfile sits beside storageProfile inside the VM's properties block.
- Confirm that the interface reference is networkInterface.id.
The final VM section defines the computer name and SSH-only Linux profile. It places the supplied public key in the planned administrator account.
- Place the cursor after the closing brace for networkProfile.
- Add the operating-system profile before the brace that closes properties with this block:
osProfile: {
computerName: '${prefix}-vm'
adminUsername: adminUsername
linuxConfiguration: {
disablePasswordAuthentication: true
ssh: {
publicKeys: [
{
path: '/home/${adminUsername}/.ssh/authorized_keys'
keyData: adminPublicKey
}
]
}
}
}
How is the VM secured?
- The profile disables password authentication with disablePasswordAuthentication: true.
- The planned account receives the public key through keyData: adminPublicKey.
- The authorized-key path uses the same adminUsername value defined near the top of the file.
- Save infrastructure.bicep.
- Confirm that osProfile sits beside networkProfile inside the VM's properties block.
- Confirm that the final public-key assignment reads keyData: adminPublicKey.
VM block still highlighted?
Check that hardwareProfile, storageProfile, networkProfile, and osProfile are siblings inside properties.
Count the closing braces around publicKeys carefully. Use the full-code comparison below to find a misplaced brace.
Ask for help with the highlighted section: help me debug the nested virtualMachine resource in infrastructure.bicep
Both files now contain the complete compute plan for this step. Use the comparison tab if you want to check every line before running What-If.
✔️ Awesome, I've got everything!
Your secure parameter flow and private compute resources are saved. Continue to the preview.
ⓧ I'd like to double check the full code
- Compare your saved main.bicep with this complete file:
targetScope = 'subscription'
@description('Azure region used for the planned resource group and resources.')
param location string = 'eastus'
@description('Name of the planned resource group.')
param resourceGroupName string = 'rg-az104-whatif'
@secure()
@description('Temporary SSH public key used only to validate the VM plan.')
param adminPublicKey string
resource labResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: resourceGroupName
location: location
tags: {
purpose: 'az104-whatif'
}
}
module infrastructure 'infrastructure.bicep' = {
name: 'az104InfrastructurePreview'
scope: labResourceGroup
params: {
location: location
prefix: 'az104lab'
adminPublicKey: adminPublicKey
}
}
What should match in main.bicep?
The file declares the secure public-key parameter at subscription scope. Its module block passes the same value into infrastructure.bicep.
- Compare your saved infrastructure.bicep with this complete file:
@description('Azure region used for the planned resources.')
param location string
@description('Short prefix used for resource names.')
param prefix string
@secure()
@description('Temporary SSH public key used only to validate the VM plan.')
param adminPublicKey string
param adminUsername string = 'az104admin'
var storageAccountName = 'st${uniqueString(resourceGroup().id)}'
var imageReference = {
publisher: 'Canonical'
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts-gen2'
version: 'latest'
}
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: '${prefix}-vnet'
location: location
properties: {
addressSpace: {
addressPrefixes: [
'10.20.0.0/16'
]
}
subnets: [
{
name: 'snet-workload'
properties: {
addressPrefix: '10.20.1.0/24'
}
}
]
}
}
resource storageAccount 'Microsoft.Storage/storageAccounts@2025-06-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
}
}
resource networkInterface 'Microsoft.Network/networkInterfaces@2023-09-01' = {
name: '${prefix}-nic'
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
subnet: {
id: virtualNetwork.properties.subnets[0].id
}
privateIPAllocationMethod: 'Dynamic'
}
}
]
}
}
resource virtualMachine 'Microsoft.Compute/virtualMachines@2023-09-01' = {
name: '${prefix}-vm'
location: location
properties: {
hardwareProfile: {
vmSize: 'Standard_D2s_v3'
}
storageProfile: {
imageReference: imageReference
osDisk: {
createOption: 'FromImage'
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
dataDisks: [
{
name: '${prefix}-data-01'
diskSizeGB: 1
lun: 0
createOption: 'Empty'
caching: 'ReadWrite'
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
]
}
networkProfile: {
networkInterfaces: [
{
id: networkInterface.id
}
]
}
osProfile: {
computerName: '${prefix}-vm'
adminUsername: adminUsername
linuxConfiguration: {
disablePasswordAuthentication: true
ssh: {
publicKeys: [
{
path: '/home/${adminUsername}/.ssh/authorized_keys'
keyData: adminPublicKey
}
]
}
}
}
}
}
What should match in infrastructure.bicep?
The file retains the original network and storage resources. It adds the secure input, image reference, private network interface, managed disks, and Linux VM profile.
Preview compute and expose the gap
The files describe the larger plan. The public key now needs to enter the secure parameter for this Cloud Shell session.
- Switch back to the Cloud Shell terminal by pressing Ctrl+`.
- Start the public-key input prompt by running these commands:
echo "Enter the SSH public key:"
read key
What does this prompt do?
The read key command waits for one line of input. Bash stores that line in key for the current Cloud Shell session.
- Paste the public key shown earlier.
- Press Enter to store it in key.
Before you run the preview, do you think the new compute plan includes every network protection the workload subnet needs?
- Run the expanded subscription-scope What-If preview with this command:
az deployment sub what-if --template-file main.bicep --parameters adminPublicKey="$key" --location eastus --validation-level Template
What does this preview evaluate?
The command evaluates both Bicep files at subscription scope. The Template validation level checks the template structure without creating resources.
The secure public-key expression may remain unevaluated in the preview. Focus on resource types, IDs, subnet properties, and the change summary.
You should see planned Create changes that include Microsoft.Compute/virtualMachines and Microsoft.Network/networkInterfaces. The preview finishes without a deployment confirmation prompt.
- Find the planned VM payload in the What-If output.
- Confirm that its network profile references the planned network interface.
- Confirm that its storage profile contains the managed OS disk.
- Confirm that its storage profile contains the empty data disk.
- Return to infrastructure.bicep in the editor.
- Inspect the snet-workload properties block.
- Confirm that the block contains addressPrefix without a networkSecurityGroup property.
The security gap is visible
The compute preview succeeds while the workload subnet has no network security group association. This valid-looking plan exposes the missing network-control boundary you were meant to detect.
Preview stopped before the change summary?
Repeat the public-key input prompt if the key value was lost. A new Cloud Shell session does not retain the previous Bash variable.
If the output points to a Bicep location, compare both files with the full-code tab. Check the braces around the VM profiles first.
Ask for help without sharing the private key: help me debug why my subscription-scope What-If preview stopped
Strong work. Your preview now proves that a compute design can pass static template validation while still carrying a visible security gap.
Next, you will add the missing subnet protection and rerun the same preview to prove the dependency chain is complete.
Protect the Subnet and Run the Final Preview
Your previous Azure Resource Manager What-If preview proved that the private VM fits the plan. It also exposed the workload subnet with no network security group association.
That missing association leaves the planned subnet without its intended network-control boundary. In this step, you will define the NSG in Bicep before validating the complete dependency chain.
In this step, get ready to:
- Define the network security group with an empty custom-rule collection.
- Associate the network security group with the workload subnet.
- Confirm the protected dependency chain in the final What-If preview.
Add the network security group
An NSG creates a control boundary for network traffic. This plan begins with no custom security rules because the workload remains private.
- In infrastructure.bicep, place the NSG resource directly above virtualNetwork by pasting this code:
resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-09-01' = {
name: '${prefix}-nsg'
location: location
properties: {
securityRules: []
}
}
What Does This Resource Do?
- The networkSecurityGroup symbolic name gives other resources a stable reference to the NSG.
- The ${prefix}-nsg expression produces the planned name az104lab-nsg.
- The empty securityRules collection adds no custom traffic rules.
- The plan still contains no public IP resource.
- Save infrastructure.bicep by pressing Ctrl+S.
- Confirm the editor shows the complete networkSecurityGroup block without red diagnostics.
Seeing a Bicep Diagnostic?
- Check that the NSG resource sits above the existing virtualNetwork resource.
- Check that every opening brace in the NSG resource has a matching closing brace.
Ask for help with the NSG resource syntax.
Associate the NSG with the workload subnet
The subnet owns the NSG association. A resource can exist in the plan without protecting a subnet until its resource ID is referenced from that subnet.
- Locate this existing line inside the snet-workload properties in infrastructure.bicep:
addressPrefix: '10.20.1.0/24'
Why Find This Line?
This line identifies the workload subnet by its 10.20.1.0/24 address prefix. It provides the exact insertion point for the association.
- Insert the NSG association directly below the address prefix so the properties look like this:
addressPrefix: '10.20.1.0/24'
networkSecurityGroup: {
id: networkSecurityGroup.id
}
How Does the Association Work?
The networkSecurityGroup property adds the missing control boundary to snet-workload.
The networkSecurityGroup.id reference also creates a dependency on az104lab-nsg.
- Save infrastructure.bicep by pressing Ctrl+S.
- Confirm networkSecurityGroup.id appears inside the snet-workload properties without red diagnostics.
Association Still Showing a Diagnostic?
- Check that networkSecurityGroup uses the same capitalization as the resource symbolic name.
- Check that the association is nested inside the properties block for snet-workload.
- Check that id: networkSecurityGroup.id remains inside the association object.
Ask for help with the subnet association.
✔️ Awesome, I've got everything!
Good work. Your saved Bicep plan now defines the NSG and associates it with the workload subnet.
ⓧ I'd like to double check the full code
- Compare your complete infrastructure.bicep file with this reference:
@description('Azure region used for the planned resources.')
param location string
@description('Short prefix used for resource names.')
param prefix string
@secure()
@description('Temporary SSH public key used only to validate the VM plan.')
param adminPublicKey string
param adminUsername string = 'az104admin'
var storageAccountName = 'st${uniqueString(resourceGroup().id)}'
var imageReference = {
publisher: 'Canonical'
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts-gen2'
version: 'latest'
}
resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-09-01' = {
name: '${prefix}-nsg'
location: location
properties: {
securityRules: []
}
}
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: '${prefix}-vnet'
location: location
properties: {
addressSpace: {
addressPrefixes: [
'10.20.0.0/16'
]
}
subnets: [
{
name: 'snet-workload'
properties: {
addressPrefix: '10.20.1.0/24'
networkSecurityGroup: {
id: networkSecurityGroup.id
}
}
}
]
}
}
resource storageAccount 'Microsoft.Storage/storageAccounts@2025-06-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
}
}
resource networkInterface 'Microsoft.Network/networkInterfaces@2023-09-01' = {
name: '${prefix}-nic'
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
subnet: {
id: virtualNetwork.properties.subnets[0].id
}
privateIPAllocationMethod: 'Dynamic'
}
}
]
}
}
resource virtualMachine 'Microsoft.Compute/virtualMachines@2023-09-01' = {
name: '${prefix}-vm'
location: location
properties: {
hardwareProfile: {
vmSize: 'Standard_D2s_v3'
}
storageProfile: {
imageReference: imageReference
osDisk: {
createOption: 'FromImage'
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
dataDisks: [
{
name: '${prefix}-data-01'
diskSizeGB: 1
lun: 0
createOption: 'Empty'
caching: 'ReadWrite'
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
]
}
networkProfile: {
networkInterfaces: [
{
id: networkInterface.id
}
]
}
osProfile: {
computerName: '${prefix}-vm'
adminUsername: adminUsername
linuxConfiguration: {
disablePasswordAuthentication: true
ssh: {
publicKeys: [
{
path: '/home/${adminUsername}/.ssh/authorized_keys'
keyData: adminPublicKey
}
]
}
}
}
}
}
What Changed in This File?
The complete module now declares networkSecurityGroup before the virtual network. The workload subnet references that resource through networkSecurityGroup.id.
- Compare your unchanged main.bicep file with this reference:
targetScope = 'subscription'
@description('Azure region used for the planned resource group and resources.')
param location string = 'eastus'
@description('Name of the planned resource group.')
param resourceGroupName string = 'rg-az104-whatif'
@secure()
@description('Temporary SSH public key used only to validate the VM plan.')
param adminPublicKey string
resource labResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: resourceGroupName
location: location
tags: {
purpose: 'az104-whatif'
}
}
module infrastructure 'infrastructure.bicep' = {
name: 'az104InfrastructurePreview'
scope: labResourceGroup
params: {
location: location
prefix: 'az104lab'
adminPublicKey: adminPublicKey
}
}
Why Does This File Stay the Same?
The subscription-scope entry file already creates the planned resource group. It also passes adminPublicKey into the infrastructure module.
Verify the protected dependency chain
The two files now describe the complete infrastructure plan. The final What-If evaluates that desired state without creating the planned resources.
This check remains cost-safe because it stops at the preview. No deployment confirmation follows the command.
- Return to the Cloud Shell terminal by pressing Ctrl+`.
Before you run this check, picture whether the workload subnet payload now contains the NSG resource ID.
- Evaluate the complete subscription plan by running this command:
az deployment sub what-if --template-file main.bicep --parameters adminPublicKey="$key" --location eastus --validation-level Template
What Should You See?
The command performs static template validation at subscription scope. It previews the complete module without making resource changes.
- You should see planned Create changes for the resource group plus the compute, storage, and networking resources.
- You should see az104lab-nsg in the planned payload.
- You should see the NSG resource ID under the workload subnet's networkSecurityGroup property.
- You should reach the change summary without a deployment confirmation prompt.
- Locate the az104lab-vm reference to az104lab-nic.
- Locate the NIC reference to the workload subnet.
- Locate the snet-workload reference to az104lab-nsg.
- Confirm the storage payload uses Standard_LRS.
- Confirm the storage payload uses TLS1_2.
- Stop after reviewing the preview.
That closes the security gap. Your final preview now proves that the private VM reaches a subnet protected by the planned NSG.
Missing the NSG Association?
- Confirm infrastructure.bicep was saved before you returned to the terminal.
- Confirm the networkSecurityGroup property sits inside the snet-workload properties.
- Confirm the association uses networkSecurityGroup.id with matching capitalization.
Ask for help interpreting the final preview.
Secret mission
Add a Management Subnet
Extend the existing virtual network with a dedicated management subnet that uses the same network security group. Then prove that both subnet ranges appear in What-If without deploying resources.
Clean Up Your Resources
Clean Up Your Resources
Your final Azure Resource Manager What-If stayed at preview, so none of the planned Azure resources were deployed. Azure Cloud Shell hosting is free in this ephemeral session because it uses no persistent storage account.
Your cleanup choice is to keep the current session, preserve the Bicep files locally, or remove the temporary files now.
Resources you used:
- The main.bicep subscription entry file written in Bicep.
- The infrastructure.bicep resource-group module.
- The temporary SSH key pair at ~/.ssh/id_rsa.pem plus ~/.ssh/id_rsa.pem.pub.
- The ephemeral Azure Cloud Shell session holding these files.
Keep everything running
No action needed. Choose this while you are still reviewing the infrastructure plan in the current Cloud Shell session.
- Keep the current Cloud Shell pane active while you continue editing the Bicep plan.
- Use Azure Resource Manager What-If for every new check.
The planned resource group, VM, disks, storage account, network interface, VNet, subnets, plus NSG remain undeployed. This keeps the documented path free of pay-as-you-go resource charges.
Pause - I'll come back to this later
An ephemeral session removes its saved files when it ends. Local copies let you return to the infrastructure plan later.
- Save a local copy of main.bicep from the Cloud Shell editor.
- Save a local copy of infrastructure.bicep from the Cloud Shell editor.
- End the current ephemeral Azure Cloud Shell session after both local files are saved.
Your reusable plan is now safe on your machine. The ephemeral session removes its temporary SSH key pair when it ends.
Delete - I don't want to use this again
Deleting these files is safe because What-If never deployed the planned Azure resources. This removes only the temporary Cloud Shell files plus the SSH key pair.
- Delete the two Bicep files plus the temporary SSH key pair by running these commands:
rm main.bicep infrastructure.bicep ~/.ssh/id_rsa.pem ~/.ssh/id_rsa.pem.pub
ls
ls ~/.ssh
What Do These Commands Do?
- The first command removes both Bicep files plus both parts of the temporary SSH key pair.
- The first ls displays the remaining files in the current Cloud Shell directory.
- The second ls displays the remaining files in the ~/.ssh directory.
You should no longer see main.bicep, infrastructure.bicep, id_rsa.pem, or id_rsa.pem.pub in the output.
Files Still Listed?
- Confirm the Bicep files' location in the Cloud Shell editor file explorer.
- Return to that location in Cloud Shell before rerunning the cleanup commands.
Ask for help checking your Cloud Shell file locations.
- End the ephemeral Azure Cloud Shell session by closing its pane in the Azure portal.
Cleanup is complete. Your pay-as-you-go subscription remains free of the resource group, VM, storage account, disks, plus networking resources from the preview.
Nice Work!
Nice Work!
You did it! You built a two-file Bicep infrastructure plan for compute, storage, and networking. Azure Resource Manager What-If validated the complete design without deploying billable resources.
You've learned how to:
- Built a subscription-scope entry point that creates a planned resource group and passes values into a reusable resource-group module.
- Planned private Azure infrastructure covering an Ubuntu VM, network interface, managed OS disk, managed data disk, storage account, virtual network, and workload subnet.
- Diagnosed a missing NSG association in the preview. You corrected the security gap and confirmed the NSG resource ID in the workload subnet.
- Completed the optional Secret Mission by adding an NSG-associated management subnet at 10.20.2.0/24 without deploying the plan.
Ready to quiz yourself?