Recover Azure Linux Ops with Bicep
Deploy, govern, and recover an Azure Linux VM with Bicep.
Introduction
30 Second Summary
A university system can feel invisible until students cannot reach it. In that moment, the operations team needs a clear path from the failed connection to the change that caused it.
In this project, you will build a governed Linux administration lab on Microsoft Azure using Bicep. You will take the lab through a deliberate SSH outage from diagnosis to recovery.
What You'll Build
Your finished demo shows an Azure Virtual Machine returning from a deliberate connectivity outage through one repeatable Bicep deployment.
By the end of this project, you'll have:
- A live SSH administration session that connects to an Ubuntu virtual machine with public-key authentication.
- Clear governance evidence showing tagged resources protected by a VM-scoped management lock.
- An incident report that traces configuration drift from Azure Monitor Activity Log evidence through Bicep recovery.
- Secret Mission: Attempt a destructive VM operation to prove the deletion lock blocks it.
Are there any prerequisites?
You need an eligible Microsoft Azure free account with a phone number plus an eligible payment card for identity verification. You also need internet access plus local administrator rights on Windows.
Before We Start
This checkpoint connects the Azure operations lab to a university systems workload. Before any hands-on work begins, you will define why reliable remote administration keeps that workload manageable during routine service or an incident.
Prepare Your Azure Workstation
Your university-style workload needs a dependable control point for routine administration. That control point is your workstation with authenticated access to Microsoft Azure.
You currently have no cloud account or Azure command-line tooling. This step creates a verified starting point before you deploy resources that can consume credit.
In this step, get ready to:
- Create an eligible Azure free account with an active subscription.
- Install and verify Azure CLI plus Bicep.
- Create the project workspace with an SSH key pair and a Bicep parameters file.
Establish your Azure command-line access
Your subscription determines which Azure resources you can create. The command-line session must use the same account so every later deployment reaches the intended subscription.
Account verification can feel sensitive because Microsoft asks for a phone number and an eligible payment card. These details verify your identity before the free subscription becomes available.
- Visit the Microsoft Azure free account page.
- Create your eligible free account using your Microsoft account.
- Complete the requested identity verification.
- Review the $200 credit boundary for the first 30 days.
- Confirm that the new subscription is active before continuing.
Know the credit boundary
Eligible new users receive $200 of Azure credit for the first 30 days. Limited quantities of selected services also remain free for 12 months.
Charges can begin after an upgrade when usage exceeds the remaining credit or free-service quantities. The cleanup section removes the lab resources when you finish.
Azure CLI lets PowerShell authenticate to your subscription. The project pins Azure CLI to version 2.90.0 so your commands match the tested workflow.
- Press the Windows key to open Windows search.
- Type PowerShell.
- Press Enter to open PowerShell.
- Check the installed Azure CLI version by running this command:
az --version
What does this command check?
The command asks Azure CLI to report its installed components and versions. Use the first Azure CLI version in the output to choose the matching tab below.
✔️ I see the required version
Azure CLI 2.90.0 is already installed. Continue below to reopen PowerShell and verify the clean session.
ⓧ I see an older version
Your current installation is below Azure CLI 2.90.0. Install the pinned release before continuing.
- Install Azure CLI 2.90.0 by running this command:
winget install --exact --id Microsoft.AzureCLI --version 2.90.0
What does this command do?
WinGet installs the official Microsoft Azure CLI package. The version option pins the package to 2.90.0.
WinGet may ask you to review source agreements. Read each prompt before accepting it.
Azure CLI installation failed?
Confirm that PowerShell has internet access. Check that WinGet is available on your Windows installation.
If the package still fails to install, help me troubleshoot the pinned Azure CLI installation.
ⓧ The command is not found
Azure CLI is not installed yet. WinGet can add the pinned Microsoft package to Windows.
- Install Azure CLI 2.90.0 by running this command:
winget install --exact --id Microsoft.AzureCLI --version 2.90.0
What does this command do?
WinGet finds the official Microsoft Azure CLI package by its package identifier. It installs version 2.90.0 on your workstation.
WinGet may ask you to review source agreements. Read each prompt before accepting it.
WinGet command missing?
Update the Windows App Installer before retrying the command. Administrative restrictions can also block package installation.
If you remain blocked, help me install Azure CLI with the official Windows installer.
Azure CLI installation updates your terminal environment. A fresh PowerShell window ensures Windows can find the new command.
- Close every open PowerShell window.
- Press the Windows key to open Windows search.
- Type PowerShell.
- Press Enter to open a fresh PowerShell session.
- Verify the pinned Azure CLI version by running this command:
az --version
What should you see?
The Azure CLI entry should report version 2.90.0. This confirms the reopened PowerShell session can find the pinned installation.
Azure CLI uses an interactive browser sign-in. The browser may ask you to choose the Microsoft account connected to your free subscription.
- Start the interactive Azure sign-in by running this command:
az login
What does this command do?
The command opens Microsoft's authentication flow in your browser. A successful sign-in returns subscription information to PowerShell.
- Choose the Microsoft account connected to the free Azure subscription.
- Complete the browser sign-in flow.
- Return to the reopened PowerShell window.
- Display the active subscription by running this command:
az account show
What does this command confirm?
The output describes the subscription Azure CLI currently targets. Confirm that it matches the active free subscription you just created.
Signed in to the wrong account?
Close the browser authentication flow if it used a different Microsoft account. Run the sign-in command again from a private browser session.
If the wrong subscription remains active, help me select the correct Azure subscription safely.
Good progress. Your reopened PowerShell session can now reach the active subscription that hosts the lab.
Install Bicep tooling
Bicep turns the lab architecture into a reusable infrastructure definition. The Bicep CLI validates that definition before Azure deploys it.
This project uses Bicep CLI version 0.47.16. Azure CLI manages the Bicep installation for this workflow.
- Check the installed Bicep CLI version in the reopened PowerShell window by running this command:
az bicep version
What does this command check?
Azure CLI checks its managed Bicep installation. The result tells you whether the required version is ready.
✔️ I see the required version
Bicep CLI 0.47.16 is already available. Continue below to verify it once more after this check.
ⓧ I see an older version
Your Bicep CLI version is below 0.47.16. Install the pinned release through Azure CLI.
- Install Bicep CLI 0.47.16 by running this command:
az bicep install --version v0.47.16
What does this command do?
Azure CLI downloads Bicep release v0.47.16 into its managed tools directory. This keeps the project on the tested Bicep version.
Bicep installation failed?
Confirm that Azure CLI still responds in the current PowerShell session. Network filtering can also prevent the Bicep download.
If the download keeps failing, help me troubleshoot the Azure CLI-managed Bicep installation.
ⓧ Bicep is not installed
Azure CLI is ready without a managed Bicep installation. Add the required release through the Azure CLI command group.
- Install Bicep CLI 0.47.16 by running this command:
az bicep install --version v0.47.16
What does this command do?
Azure CLI downloads Bicep release v0.47.16 for use by later deployment commands.
Bicep download blocked?
Check that the workstation can reach Microsoft services. Corporate network policies can block command-line downloads.
If you need a hand, help me diagnose why Azure CLI cannot install Bicep.
- Verify the Bicep CLI installation by running this command:
az bicep version
What should you see?
The output should report Bicep CLI version 0.47.16. This confirms Azure CLI can invoke the required Bicep release.
The Microsoft Bicep extension adds syntax validation and resource information to Visual Studio Code. It catches template mistakes while you edit the lab files.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Visual Studio Code.
- Press Enter to open Visual Studio Code.
- Select View from the top menu.
- Select Extensions.
- Search for bicep.
- Select the Microsoft Bicep extension.
- Select Install.
The extension page should show that Microsoft Bicep is installed. Your editor can now validate the infrastructure files created in the next step.
Extension installation unavailable?
Confirm that Visual Studio Code can access the extension marketplace. Managed workstations may restrict extension installation.
If the extension remains unavailable, help me troubleshoot the Microsoft Bicep extension installation.
Create the project files and SSH key
A dedicated workspace keeps the reusable Bicep files together. The SSH key pair remains in your home directory so the private key never enters the project folder.
- Select File in Visual Studio Code.
- Select Open Folder....
- Choose your Desktop in the folder picker.
- Select New Folder.
- Enter governed-azure-linux-lab as the folder name.
- Select Select Folder on Windows or Open on macOS.
You should see governed-azure-linux-lab at the top of the Explorer view. This visible folder name confirms that Visual Studio Code is using the new workspace.
OpenSSH creates a private key for authentication and a public key for the virtual machine. The private key provides access to the VM, so protect it like a password.
Protect any existing key
The next command writes to ~/.ssh/id_rsa.pem. If Git Bash reports that this file already exists, stop before confirming an overwrite.
Store any existing key somewhere secure before retrying. Never paste the private key into a project file.
- Press the Windows key to open Windows search.
- Type Git Bash.
- Press Enter to open Git Bash.
- Generate the 4096-bit RSA PEM 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 creates the private key at ~/.ssh/id_rsa.pem. It creates the matching public key at ~/.ssh/id_rsa.pem.pub.
Git Bash may ask for a passphrase. A passphrase adds local protection to the private key.
- Enter a private passphrase when prompted.
- Enter the same passphrase again when prompted.
- Display the public key line by running this command:
cat ~/.ssh/id_rsa.pem.pub
What should you copy?
The output is one public-key line beginning with ssh-rsa. Copy the entire line without adding spaces or line breaks.
The public key is safe to place in the Bicep parameters file. The private key stays only at ~/.ssh/id_rsa.pem.
- Return to the Visual Studio Code window from earlier.
- Select the Explorer view in the Activity Bar.
- Select New File... in the Explorer view.
- Enter main.bicepparam as the file name.
- Add the parameter-file structure by pasting this code:
using './main.bicep'
param sshPublicKey = 'paste-the-full-public-key-line-here'
What does this file do?
The using declaration connects the parameters file to main.bicep. That template is created in the next step.
The sshPublicKey parameter supplies the public-key line that Azure places on the Linux VM.
- Replace paste-the-full-public-key-line-here with the complete public-key line you copied.
- Press Cmd+S on macOS or Ctrl+S on Windows to save main.bicepparam.
- Confirm that the Explorer view lists main.bicepparam inside governed-azure-linux-lab.
The reference below uses the privacy-safe placeholder from the project template. Your local copy should contain your public-key line in that position.
✔️ My parameter file is ready
Your saved file contains the two-line structure shown in the reference. Your full public key replaces the placeholder value.
ⓧ I'd like to double check the full code
using './main.bicep'
param sshPublicKey = 'paste-the-full-public-key-line-here'
How should your file differ?
Keep the using declaration and parameter name exactly as shown. Replace only the placeholder between the single quotation marks with your public-key line.
Key pair or parameter file missing?
Confirm that Git Bash reported both key file paths after generation. Check that main.bicepparam appears directly inside governed-azure-linux-lab.
If the public key or parameter file still looks wrong, help me verify my SSH key paths and Bicep parameter structure.
Before you run the final check, which Azure CLI version and Bicep CLI version do you expect the reopened PowerShell session to report?
- Return to the reopened PowerShell window from earlier.
- Verify the complete workstation state by running these commands:
az --version
az bicep version
az account show
What does the final check prove?
The first two commands confirm the pinned command-line tools. The final command confirms which Azure subscription receives later deployments.
You should see Azure CLI 2.90.0. You should also see Bicep CLI 0.47.16.
The account output should identify the active free subscription you created. That result confirms your workstation is authenticated to the intended Azure environment.
Your Azure workstation is ready. Next, you will turn the empty workspace into a reachable Linux administration lab.
Deploy the Linux Administration Lab
Your Microsoft Azure workstation is ready. Your command-line tools are verified.
Now the project needs a reachable Linux host for administration work. Bicep turns that lab into reusable source.
The first deployment gives you a visible working lab. Governance metadata is deliberately absent.
Deletion protection is also deliberately absent. That gap becomes evidence in the next step.
In this step, get ready to:
- Define the lab network and Ubuntu virtual machine in main.bicep.
- Deploy the infrastructure to rg-cbt-status-lab.
- Connect to cbt-status-vm through SSH.
Define the lab infrastructure
A virtual network gives the VM private address space. A network security group controls the inbound path to its subnet.
A public IP address provides the external endpoint. A network interface connects that endpoint to the virtual machine.
Why Bicep instead of portal clicks?
Bicep stores the desired Azure configuration in main.bicep. The same file can recreate missing infrastructure later.
Portal-only changes leave no reusable definition for recovery. A source file makes configuration drift visible.
- Select the new-file control in the Visual Studio Code Explorer sidebar.
- Name the file main.bicep.
You should see main.bicep beside main.bicepparam in the project folder.
- Build the parameter and naming layer by pasting this into main.bicep:
@description('Azure region for all lab resources.')
param location string = resourceGroup().location
@description('Administrator username for the Linux virtual machine.')
param adminUsername string = 'azureoperator'
@description('Full SSH public key line used for key-based authentication.')
param sshPublicKey string
var vmName = 'cbt-status-vm'
var vnetName = 'cbt-status-vnet'
var subnetName = 'management-subnet'
var nsgName = 'cbt-status-nsg'
var publicIpName = 'cbt-status-pip'
var nicName = 'cbt-status-nic'
What does this code define?
- The parameters receive the Azure region and SSH public key.
- The administrator defaults to azureoperator.
- The variables keep every Azure resource name consistent.
- Save main.bicep.
- Check the diagnostics area below the editor.
You should see Bicep syntax highlighting with no red syntax errors.
Seeing an error in the declarations?
Check each quote around the default values. Confirm each parameter name matches the snippet.
Still stuck? Help me fix the parameter or variable error in my main.bicep file.
- Add the SSH access control below the naming variables by pasting:
resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-09-01' = {
name: nsgName
location: location
properties: {
securityRules: [
{
name: 'allow-ssh'
properties: {
access: 'Allow'
description: 'Allow temporary SSH administration for the portfolio lab.'
destinationAddressPrefix: '*'
destinationPortRange: '22'
direction: 'Inbound'
priority: 100
protocol: 'Tcp'
sourceAddressPrefix: 'Internet'
sourcePortRange: '*'
}
}
]
}
}
What does the SSH rule do?
- The network security group contains one inbound rule named allow-ssh.
- The rule permits TCP traffic to port 22 from the internet.
- Priority 100 makes this rule eligible before lower-priority rules.
- Save main.bicep.
- Check the diagnostics area below the editor.
You should see no red syntax errors in the networkSecurityGroup resource.
Is the security rule underlined?
Check the braces around securityRules. Confirm the destination port remains a quoted value.
Need another pair of eyes? Help me diagnose my Bicep network security group resource.
- Add the private network below the network security group by pasting:
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: vnetName
location: location
properties: {
addressSpace: {
addressPrefixes: [
'10.10.0.0/16'
]
}
subnets: [
{
name: subnetName
properties: {
addressPrefix: '10.10.1.0/24'
networkSecurityGroup: {
id: networkSecurityGroup.id
}
}
}
]
}
}
How is the network arranged?
- The virtual network reserves 10.10.0.0/16 for the lab.
- The management subnet uses 10.10.1.0/24 inside that range.
- The subnet references the network security group through its Azure resource ID.
- Save main.bicep.
- Check the diagnostics area below the editor.
You should see both network resources without red syntax errors.
Is the subnet reference failing?
Confirm networkSecurityGroup.id matches the resource name declared above. Check the brackets around the subnet array.
Still seeing a diagnostic? Help me fix the virtual network and subnet definitions in Bicep.
- Add the external endpoint below the virtual network by pasting:
resource publicIPAddress 'Microsoft.Network/publicIPAddresses@2023-09-01' = {
name: publicIpName
location: location
sku: {
name: 'Standard'
tier: 'Regional'
}
properties: {
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static'
}
}
Why use this public IP?
- The Standard SKU creates the public endpoint used by the SSH client.
- Static allocation keeps the address assigned to this public IP resource.
- The endpoint uses IPv4 in the Azure region selected for the resource group.
- Save main.bicep.
- Check the diagnostics area below the editor.
You should see the publicIPAddress resource without red syntax errors.
Is the public IP resource invalid?
Check the spelling of publicIPAddressVersion. Confirm the SKU block sits outside the properties block.
Need help checking the structure? Help me troubleshoot my Bicep public IP resource.
- Connect the public endpoint to the subnet by adding this network interface:
resource networkInterface 'Microsoft.Network/networkInterfaces@2023-09-01' = {
name: nicName
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
privateIPAllocationMethod: 'Dynamic'
publicIPAddress: {
id: publicIPAddress.id
}
subnet: {
id: virtualNetwork.properties.subnets[0].id
}
}
}
]
}
}
What does the interface connect?
- The network interface receives a dynamic private address inside the management subnet.
- The publicIPAddress.id reference attaches the public endpoint.
- The subnet reference places the interface behind the SSH security rule.
- Save main.bicep.
- Check the diagnostics area below the editor.
You should see the network interface with valid references to both network resources.
Are the interface references unresolved?
Confirm the public IP resource appears above this block. Confirm the virtual network resource uses the exact name virtualNetwork.
Still seeing an unresolved reference? Help me repair the network interface references in my Bicep file.
The VM resource brings the compute settings into the network you just declared. It uses Ubuntu 22.04 LTS with key-based SSH access.
- Assemble the virtualMachine resource from the next three blocks before saving main.bicep.
- Place your cursor below the networkInterface resource.
- Paste the first VM section:
resource virtualMachine 'Microsoft.Compute/virtualMachines@2023-09-01' = {
name: vmName
location: location
properties: {
hardwareProfile: {
vmSize: 'Standard_B1s'
}
networkProfile: {
networkInterfaces: [
{
id: networkInterface.id
properties: {
deleteOption: 'Delete'
primary: true
}
}
]
}
What does the first VM section do?
This section selects the Standard_B1s VM size. It attaches the network interface as the primary connection.
The resource remains open for the operating system settings in the next block.
- Continue directly below the networkProfile block by pasting:
osProfile: {
computerName: vmName
adminUsername: adminUsername
linuxConfiguration: {
disablePasswordAuthentication: true
provisionVMAgent: true
ssh: {
publicKeys: [
{
keyData: sshPublicKey
path: '/home/${adminUsername}/.ssh/authorized_keys'
}
]
}
}
}
How is access secured?
- The VM hostname comes from vmName.
- The administrator comes from adminUsername.
- The public key is written to the administrator's authorized-keys file.
- Password authentication stays disabled.
- Finish the VM resource by pasting the final section directly below osProfile:
storageProfile: {
imageReference: {
publisher: 'Canonical'
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts-gen2'
version: 'latest'
}
osDisk: {
caching: 'ReadWrite'
createOption: 'FromImage'
deleteOption: 'Delete'
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
}
diagnosticsProfile: {
bootDiagnostics: {
enabled: true
}
}
}
}
output publicIpAddress string = publicIPAddress.properties.ipAddress
output administrator string = adminUsername
output sshCommand string = 'ssh -i ~/.ssh/id_rsa.pem ${adminUsername}@${publicIPAddress.properties.ipAddress}'
What completes the VM?
- The image reference selects Ubuntu 22.04 LTS.
- The managed operating-system disk uses Standard_LRS storage.
- Boot diagnostics provide startup information for later troubleshooting.
- The outputs expose the public address and administrator name.
- The sshCommand output assembles the connection command.
- Save main.bicep.
- Check the diagnostics area below the editor.
You should see a complete VM resource with no unclosed-brace errors.
Seeing an error near the VM resource?
Confirm the three VM sections are contiguous. Check that the final section closes the properties block before it closes the resource.
Need help matching the braces? Help me find the structural error in my Bicep virtual machine resource.
The project documentation maps the full operating model. Some listed governance controls arrive in later steps.
- Select the new-file control in the Visual Studio Code Explorer sidebar.
- Name the file README.md.
- Document the lab by pasting:
# Governed Azure Linux Operations Lab
This portfolio project deploys a Linux administration lab to Microsoft Azure with Bicep.
## Scenario
The environment represents a small university operations workload that must remain remotely manageable, consistently governed, and recoverable after an accidental network configuration change.
## Architecture
Internet SSH client -> Standard public IP -> management subnet NSG -> Ubuntu virtual machine
## Administrative controls
- SSH public-key authentication
- Password authentication disabled
- Reusable Bicep deployment
- Environment, workload, and ownership-method tags
- VM-scoped CanNotDelete lock
- Azure Monitor Activity Log evidence
- What-if preview before recovery
## Portfolio evidence
- `main.bicep`: desired Azure infrastructure state
- `main.bicepparam`: deployment parameter file with the learner's public key
- `incident-report.md`: outage diagnosis and recovery record
What does the README capture?
The README connects the university workload scenario to the Azure architecture. It also maps the evidence that the finished project produces.
The controls list acts as a roadmap for the remaining steps.
- Save README.md.
- Confirm README.md appears in the Explorer sidebar.
You should now see the project description beside both Bicep files.
Does the README preview look incomplete?
Check that each heading starts on its own line. Confirm each list item begins with a hyphen.
Need help with the Markdown structure? Help me check the formatting in my project README.
✔️ Awesome, I've got everything!
Great. Save every project file before moving to validation.
ⓧ I'd like to double check the full code
Compare each file with the references below. Keep your actual SSH public-key line in your local parameter file.
# Governed Azure Linux Operations Lab
This portfolio project deploys a Linux administration lab to Microsoft Azure with Bicep.
## Scenario
The environment represents a small university operations workload that must remain remotely manageable, consistently governed, and recoverable after an accidental network configuration change.
## Architecture
Internet SSH client -> Standard public IP -> management subnet NSG -> Ubuntu virtual machine
## Administrative controls
- SSH public-key authentication
- Password authentication disabled
- Reusable Bicep deployment
- Environment, workload, and ownership-method tags
- VM-scoped CanNotDelete lock
- Azure Monitor Activity Log evidence
- What-if preview before recovery
## Portfolio evidence
- `main.bicep`: desired Azure infrastructure state
- `main.bicepparam`: deployment parameter file with the learner's public key
- `incident-report.md`: outage diagnosis and recovery record
@description('Azure region for all lab resources.')
param location string = resourceGroup().location
@description('Administrator username for the Linux virtual machine.')
param adminUsername string = 'azureoperator'
@description('Full SSH public key line used for key-based authentication.')
param sshPublicKey string
var vmName = 'cbt-status-vm'
var vnetName = 'cbt-status-vnet'
var subnetName = 'management-subnet'
var nsgName = 'cbt-status-nsg'
var publicIpName = 'cbt-status-pip'
var nicName = 'cbt-status-nic'
resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-09-01' = {
name: nsgName
location: location
properties: {
securityRules: [
{
name: 'allow-ssh'
properties: {
access: 'Allow'
description: 'Allow temporary SSH administration for the portfolio lab.'
destinationAddressPrefix: '*'
destinationPortRange: '22'
direction: 'Inbound'
priority: 100
protocol: 'Tcp'
sourceAddressPrefix: 'Internet'
sourcePortRange: '*'
}
}
]
}
}
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: vnetName
location: location
properties: {
addressSpace: {
addressPrefixes: [
'10.10.0.0/16'
]
}
subnets: [
{
name: subnetName
properties: {
addressPrefix: '10.10.1.0/24'
networkSecurityGroup: {
id: networkSecurityGroup.id
}
}
}
]
}
}
resource publicIPAddress 'Microsoft.Network/publicIPAddresses@2023-09-01' = {
name: publicIpName
location: location
sku: {
name: 'Standard'
tier: 'Regional'
}
properties: {
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static'
}
}
resource networkInterface 'Microsoft.Network/networkInterfaces@2023-09-01' = {
name: nicName
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
privateIPAllocationMethod: 'Dynamic'
publicIPAddress: {
id: publicIPAddress.id
}
subnet: {
id: virtualNetwork.properties.subnets[0].id
}
}
}
]
}
}
resource virtualMachine 'Microsoft.Compute/virtualMachines@2023-09-01' = {
name: vmName
location: location
properties: {
hardwareProfile: {
vmSize: 'Standard_B1s'
}
networkProfile: {
networkInterfaces: [
{
id: networkInterface.id
properties: {
deleteOption: 'Delete'
primary: true
}
}
]
}
osProfile: {
computerName: vmName
adminUsername: adminUsername
linuxConfiguration: {
disablePasswordAuthentication: true
provisionVMAgent: true
ssh: {
publicKeys: [
{
keyData: sshPublicKey
path: '/home/${adminUsername}/.ssh/authorized_keys'
}
]
}
}
}
storageProfile: {
imageReference: {
publisher: 'Canonical'
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts-gen2'
version: 'latest'
}
osDisk: {
caching: 'ReadWrite'
createOption: 'FromImage'
deleteOption: 'Delete'
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
}
diagnosticsProfile: {
bootDiagnostics: {
enabled: true
}
}
}
}
output publicIpAddress string = publicIPAddress.properties.ipAddress
output administrator string = adminUsername
output sshCommand string = 'ssh -i ~/.ssh/id_rsa.pem ${adminUsername}@${publicIPAddress.properties.ipAddress}'
using './main.bicep'
param sshPublicKey = 'paste-the-full-public-key-line-here'
Validate and deploy the template
Linting catches structural problems before Azure receives the deployment. The parameter file from earlier supplies your public key without placing the private key in the project.
- Return to the PowerShell session signed in to Azure.
- Move to the local project folder from earlier.
- Validate main.bicep by running:
az bicep lint --file .\main.bicep
A clean result contains no Bicep lint errors. Your template is ready for Azure to evaluate.
Did the lint command report a problem?
Use the reported line number to compare that section with the full-code reference. Confirm the file is saved before retrying.
Still blocked? Help me interpret my Bicep lint result.
This deployment starts consuming Azure credit. The cleanup section removes the resources when you finish.
- Create rg-cbt-status-lab in eastus by running:
az group create --name rg-cbt-status-lab --location eastus
You should see a response identifying rg-cbt-status-lab in eastus.
Could Azure not create the resource group?
Confirm the active subscription permits resource creation. Check that PowerShell still has an authenticated Azure CLI session.
Need help with the account context? Help me diagnose why my Azure resource group creation failed.
- Select main.bicepparam in the Visual Studio Code Explorer sidebar.
- Confirm sshPublicKey still contains your full public-key line.
- Return to the PowerShell session in the project folder.
The deployment can take several minutes while Azure provisions the VM. Quiet periods are normal during this build.
- Deploy the parameter file as deploy-cbt-status by running:
az deployment group create --name deploy-cbt-status --resource-group rg-cbt-status-lab --parameters .\main.bicepparam
You should see a successful deployment result. The result includes publicIpAddress, administrator, and sshCommand outputs.
- Record the sshCommand deployment output here: your SSH command.
What did Azure create?
The deployment created the network path from the public IP through cbt-status-nsg to cbt-status-vm.
The VM accepts your public key as azureoperator. Password authentication remains disabled.
Did the deployment fail?
Confirm the lint command completed without errors. Check that main.bicepparam contains one complete public-key line.
If Azure reports a regional capacity or quota issue for Standard_B1s, resolve that subscription constraint before retrying.
Need help with the deployment result? Help me diagnose my Azure Bicep deployment failure.
Connect to the VM over SSH
The deployment result proves that Azure accepted the desired state. An SSH session proves that the administration path works from end to end.
- Press the Windows key to open search.
- Type Git Bash and press Enter.
The first connection can ask you to confirm the new host identity. Follow the on-screen prompt to continue.
Before you connect, do you expect the prompt to show your local computer or the Azure VM?
- Start the SSH session by running the command you recorded:
[[SSH_COMMAND="your SSH command"]]
You should see a remote prompt containing azureoperator@cbt-status-vm. That prompt proves the public endpoint and SSH rule are working.
Did the SSH connection fail?
Confirm you copied the complete sshCommand output. Check that ~/.ssh/id_rsa.pem still exists in Git Bash.
Confirm the deployment completed successfully before retrying. A VM that is still provisioning cannot accept the session yet.
Still unable to connect? Help me troubleshoot the SSH path to cbt-status-vm.
That is the first live win: your Azure-hosted Linux administration lab is accepting remote sessions. Next, you will expose its missing governance controls.
Add Governance Controls
Your Azure Linux lab is running from Bicep. You can reach the virtual machine through SSH.
A working virtual machine can still sit outside your governance standards. Right now, a query for resource tags finds nothing.
The virtual machine also has no management lock to protect it from accidental deletion. You will expose both gaps before adding the missing controls.
In this step, get ready to:
- Expose the missing governance metadata with a tag query.
- Declare governance metadata plus deletion protection in main.bicep.
- Apply the governed desired state to Azure.
Expose the governance gap
The ManagedBy=Bicep filter should identify every resource managed through your template. Your initial deployment has no matching tags.
Before you run the query, consider this question: will the filter find any Bicep-managed resources.
- Return to your signed-in PowerShell session.
- Query the resource group for the governance tag by running this command:
az resource list --resource-group rg-cbt-status-lab --tag ManagedBy=Bicep --output table
What does this query check?
- The resource-group filter limits the query to rg-cbt-status-lab.
- The tag filter asks Azure CLI for resources whose ManagedBy value is Bicep.
- The table format makes matching resources easy to identify.
You will see no matching resource rows. That empty result is the intended governance gap.
Declare tags and deletion protection
Tags give each directly managed resource consistent ownership metadata. The lock adds an independent control that blocks deletion of cbt-status-vm.
- Return to the open main.bicep file in Visual Studio Code.
- Locate param sshPublicKey string.
- Add the governance parameter immediately below that line by copying this code:
@description('Governance tags applied to the resources created directly by this template.')
param resourceTags object = {
Environment: 'Portfolio'
Workload: 'CBT-Operations-Lab'
ManagedBy: 'Bicep'
}
What does this parameter define?
- Environment identifies the lab as a portfolio workload.
- Workload groups the resources under CBT-Operations-Lab.
- ManagedBy records Bicep as the desired-state owner.
The template creates five resources directly: networkSecurityGroup, virtualNetwork, publicIPAddress, networkInterface, plus virtualMachine.
- Add tags: resourceTags directly below location: location in each of those five resource declarations.
- Save main.bicep.
- Check the tag declarations by running the Bicep linter in PowerShell:
az bicep lint --file .\main.bicep
What does this check do?
The linter reads the updated template before Azure receives it. A clean result confirms that the parameter and tag properties use valid Bicep syntax.
PowerShell should return control without reporting a problem in main.bicep.
Seeing a Bicep problem?
- Confirm that every tags: resourceTags line has the same indentation as its neighbouring location: location line.
- Confirm that you added tags only inside the five directly created resource declarations.
If the diagnostic still does not make sense, ask for help interpreting the Bicep lint result.
A CanNotDelete lock allows normal reads plus updates. It blocks deletion of the resource in its scope.
- Locate output publicIpAddress string = publicIPAddress.properties.ipAddress near the bottom of main.bicep.
- Add the VM-scoped lock immediately above that output by copying this code:
resource vmLock 'Microsoft.Authorization/locks@2020-05-01' = {
name: 'protect-cbt-status-vm'
scope: virtualMachine
properties: {
level: 'CanNotDelete'
notes: 'Protect the portfolio VM from accidental deletion.'
}
}
How does this lock work?
- The scope connects the lock directly to virtualMachine.
- The name identifies the control as protect-cbt-status-vm.
- The CanNotDelete level prevents deletion while leaving supported updates available.
- Save main.bicep.
- Validate the completed governance code by running the linter again:
az bicep lint --file .\main.bicep
What does the final lint prove?
This check parses the completed template with its tags plus lock. A clean result means the governance declarations are ready for a deployment preview.
PowerShell should return control without reporting a Bicep problem.
Lock declaration not passing lint?
- Confirm that vmLock sits after the closing brace of virtualMachine.
- Confirm that scope: virtualMachine remains inside the vmLock declaration.
If the declaration still fails, ask for help comparing the lock block with the surrounding Bicep structure.
✔️ Awesome, I've got everything!
- Confirm that main.bicep is saved.
ⓧ I'd like to double check the full code
@description('Azure region for all lab resources.')
param location string = resourceGroup().location
@description('Administrator username for the Linux virtual machine.')
param adminUsername string = 'azureoperator'
@description('Full SSH public key line used for key-based authentication.')
param sshPublicKey string
@description('Governance tags applied to the resources created directly by this template.')
param resourceTags object = {
Environment: 'Portfolio'
Workload: 'CBT-Operations-Lab'
ManagedBy: 'Bicep'
}
var vmName = 'cbt-status-vm'
var vnetName = 'cbt-status-vnet'
var subnetName = 'management-subnet'
var nsgName = 'cbt-status-nsg'
var publicIpName = 'cbt-status-pip'
var nicName = 'cbt-status-nic'
resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-09-01' = {
name: nsgName
location: location
tags: resourceTags
properties: {
securityRules: [
{
name: 'allow-ssh'
properties: {
access: 'Allow'
description: 'Allow temporary SSH administration for the portfolio lab.'
destinationAddressPrefix: '*'
destinationPortRange: '22'
direction: 'Inbound'
priority: 100
protocol: 'Tcp'
sourceAddressPrefix: 'Internet'
sourcePortRange: '*'
}
}
]
}
}
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: vnetName
location: location
tags: resourceTags
properties: {
addressSpace: {
addressPrefixes: [
'10.10.0.0/16'
]
}
subnets: [
{
name: subnetName
properties: {
addressPrefix: '10.10.1.0/24'
networkSecurityGroup: {
id: networkSecurityGroup.id
}
}
}
]
}
}
resource publicIPAddress 'Microsoft.Network/publicIPAddresses@2023-09-01' = {
name: publicIpName
location: location
tags: resourceTags
sku: {
name: 'Standard'
tier: 'Regional'
}
properties: {
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static'
}
}
resource networkInterface 'Microsoft.Network/networkInterfaces@2023-09-01' = {
name: nicName
location: location
tags: resourceTags
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
privateIPAllocationMethod: 'Dynamic'
publicIPAddress: {
id: publicIPAddress.id
}
subnet: {
id: virtualNetwork.properties.subnets[0].id
}
}
}
]
}
}
resource virtualMachine 'Microsoft.Compute/virtualMachines@2023-09-01' = {
name: vmName
location: location
tags: resourceTags
properties: {
hardwareProfile: {
vmSize: 'Standard_B1s'
}
networkProfile: {
networkInterfaces: [
{
id: networkInterface.id
properties: {
deleteOption: 'Delete'
primary: true
}
}
]
}
osProfile: {
computerName: vmName
adminUsername: adminUsername
linuxConfiguration: {
disablePasswordAuthentication: true
provisionVMAgent: true
ssh: {
publicKeys: [
{
keyData: sshPublicKey
path: '/home/${adminUsername}/.ssh/authorized_keys'
}
]
}
}
}
storageProfile: {
imageReference: {
publisher: 'Canonical'
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts-gen2'
version: 'latest'
}
osDisk: {
caching: 'ReadWrite'
createOption: 'FromImage'
deleteOption: 'Delete'
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
}
diagnosticsProfile: {
bootDiagnostics: {
enabled: true
}
}
}
}
resource vmLock 'Microsoft.Authorization/locks@2020-05-01' = {
name: 'protect-cbt-status-vm'
scope: virtualMachine
properties: {
level: 'CanNotDelete'
notes: 'Protect the portfolio VM from accidental deletion.'
}
}
output publicIpAddress string = publicIPAddress.properties.ipAddress
output administrator string = adminUsername
output sshCommand string = 'ssh -i ~/.ssh/id_rsa.pem ${adminUsername}@${publicIPAddress.properties.ipAddress}'
What should match?
Compare the parameter block plus all five tag properties with your file. Confirm that vmLock appears between virtualMachine and the deployment outputs.
Preview and apply the governed state
A what-if operation compares your Bicep source with the current Azure state. It lets you inspect the governance changes before the deployment applies them.
Before you run the preview, consider this question: which existing resources should change and which new control should appear.
- Return to your signed-in PowerShell session.
- Preview the governance changes by running this command:
az deployment group what-if --resource-group rg-cbt-status-lab --parameters .\main.bicepparam
What should the preview show?
- The existing network plus compute resources should receive the declared tags.
- A new resource-level lock should be created for the virtual machine.
- The existing SSH rule should remain part of the declared network security group.
The preview should describe tag updates plus the new VM lock. Your virtual machine remains available during these governance changes.
Applying the deployment can take a few minutes while Azure updates the resource metadata. The command returns when the declared state has been applied.
- Redeploy the existing parameter file by running this command:
az deployment group create --name deploy-cbt-status --resource-group rg-cbt-status-lab --parameters .\main.bicepparam
What does this deployment apply?
- The deployment updates the five directly managed resources with resourceTags.
- The deployment creates protect-cbt-status-vm at the virtual-machine scope.
- The deployment keeps main.bicep as the desired-state source of truth.
That is the governance layer in place. Your running lab now carries consistent metadata plus deletion protection.
Deployment did not complete?
- Confirm that PowerShell is still signed in to the subscription containing rg-cbt-status-lab.
- Confirm that main.bicepparam still points to main.bicep.
- Review the deployment output for the resource whose update failed.
For environment-specific deployment help, ask for help diagnosing the Azure deployment output.
Before the final check, consider this question: what evidence would prove that Azure applied both governance controls.
- Verify the resource tags plus VM lock by running these commands:
az resource list --resource-group rg-cbt-status-lab --tag ManagedBy=Bicep --output table
az resource lock list -g rg-cbt-status-lab --resource cbt-status-vm --resource-type Microsoft.Compute/virtualMachines --output table
What should you see?
The first table should list five directly managed resources.
- The network security group appears as cbt-status-nsg.
- The virtual network appears as cbt-status-vnet.
- The public IP address appears as cbt-status-pip.
- The network interface appears as cbt-status-nic.
- The virtual machine appears as cbt-status-vm.
The second table should show protect-cbt-status-vm with the CanNotDelete level.
Strong work. The tag query now finds your Bicep-managed resources. The lock query confirms that the VM has deletion protection.
Your lab is governed without losing SSH access. Next, you will create a network outage and use Azure evidence to diagnose the drift.
Diagnose the SSH Outage
Your Azure lab now accepts key-based SSH connections. The last step added its governance baseline.
A working environment does not prove your troubleshooting skill. You will deliberately remove the SSH rule to create a real connectivity failure.
The live network state reveals what disappeared. Azure Monitor Activity Log reveals when the administrative change occurred.
In this step, get ready to:
- Remove the SSH network rule to reproduce a remote administration outage.
- Compare the live network security group with the unchanged Bicep desired state.
- Correlate the network change with Azure Monitor Activity Log evidence.
Reproduce the SSH outage
The deployed NSG currently allows inbound TCP traffic on port 22 through allow-ssh. Deleting that rule creates configuration drift because main.bicep still declares it.
- Close every Git Bash window that currently shows the remote azureoperator@cbt-status-vm prompt.
The existing remote prompt is gone. Your next SSH attempt starts a fresh connection.
- Return to PowerShell from earlier.
- Delete allow-ssh from the deployed NSG by running this command:
az network nsg rule delete -g rg-cbt-status-lab --nsg-name cbt-status-nsg -n allow-ssh
What did this command change?
The command removes allow-ssh from the deployed cbt-status-nsg network security group.
It leaves main.bicep unchanged. The live environment now differs from the desired state.
Before you retry the connection, do you expect the new SSH session to reach the VM?
- Press the Windows key to open the search bar.
- Type Git Bash into the search bar.
- Press Enter to open a fresh Git Bash window.
- Press the Up Arrow key until your previous SSH command appears.
- Press Enter to retry the SSH connection.
The new SSH connection cannot establish a remote session. This intended failure proves that the live network path no longer permits inbound SSH traffic.
SSH still connects?
- Confirm that you tested from a fresh Git Bash window. An already-connected session does not prove that a new connection is allowed.
- Confirm that PowerShell completed the rule deletion without reporting an error.
- Repeat the deletion command if another Bicep deployment recreated the rule.
Still reaching the VM? Help me diagnose why the new SSH connection still succeeds.
Confirm the missing network rule
The failed SSH connection is the symptom. The deployed NSG rule list reveals the network control that changed.
Before you inspect the rules, which declared rule do you expect to be missing?
- List the deployed rules in cbt-status-nsg by running this command in PowerShell:
az network nsg rule list -g rg-cbt-status-lab --nsg-name cbt-status-nsg --output table
What does this evidence prove?
- The query reads the rules currently deployed in cbt-status-nsg.
- The table represents the live NSG state after the deletion.
- Comparing that state with main.bicep separates configuration drift from a source-code error.
- Scan the table for a row named allow-ssh.
- Switch back to main.bicep in Visual Studio Code from earlier.
- Find the unchanged line name: 'allow-ssh'.
You should see no allow-ssh row in the deployed rule table. Your local main.bicep file still declares the rule.
Still see the SSH rule?
- Check that the query targets rg-cbt-status-lab.
- Check that the query targets cbt-status-nsg.
- Repeat the deletion command from the previous substep if the rule was recreated.
Need help finding the source of the mismatch? Help me compare the deployed NSG rules with my Bicep template.
Trace the change in Activity Log
Activity Log records control-plane operations against Azure resources. The evidence window connects the recent NSG change to the connectivity failure.
- Return to the PowerShell window from earlier.
Before you query the previous hour, do you expect the network rule deletion to appear among the recent administrative changes?
- Query the resource group's recent Activity Log events by running this command:
az monitor activity-log list -g rg-cbt-status-lab --offset 1h --output table
How does Activity Log help?
The one-hour window narrows the evidence to events surrounding the outage.
A recent network security rule deletion connects the control-plane change to the failed SSH connection.
- Review the returned events for the recent administrative network security rule deletion.
- Match the event time to the failed SSH attempt.
You should see the recent administrative change associated with the missing rule. The rule list provides the current-state evidence. Activity Log provides the change history.
Can't find the deletion event?
- Confirm that the deletion occurred within the previous hour covered by the query.
- Review every returned row for the network security rule change.
- Confirm that PowerShell is signed in to the subscription containing rg-cbt-status-lab.
Still missing the event? Help me locate the NSG rule deletion in Azure Monitor Activity Log.
You traced the failed SSH connection to the missing NSG rule. Next, you will use the unchanged Bicep desired state to restore service.
Restore Service with Bicep
You traced the SSH outage to the deleted allow-ssh rule. The next goal is to restore remote administration without making a one-off repair.
A manual rule change would hide the configuration drift you just diagnosed. Bicep lets you preview the declared repair before applying it.
In this step, get ready to:
- Preview the difference between the deployed resources and main.bicep.
- Restore remote administration from the declared Bicep state.
- Record the outage evidence and recovery in incident-report.md.
Measure the drift
A what-if operation compares the declared Bicep state with the current Azure state. It reveals the planned change without modifying the resource group.
Before you run the preview, do you think Bicep still recognizes the missing rule as part of the desired state?
- Preview the difference between the live resource group and main.bicepparam by running this command in the PowerShell terminal from earlier:
az deployment group what-if --resource-group rg-cbt-status-lab --parameters .\main.bicepparam
What does the preview prove?
- The what-if operation reads the desired resources through main.bicepparam.
- It compares those declarations with the resources currently deployed in rg-cbt-status-lab.
- It identifies changes before a deployment applies them.
You should see a planned creation for the missing allow-ssh network security group rule. This proves that main.bicep still contains the repair.
Missing the planned SSH rule?
- Confirm the command references rg-cbt-status-lab.
- Confirm main.bicepparam still begins with using './main.bicep'.
- Confirm main.bicep still declares the rule named allow-ssh.
If the preview still does not include the missing rule, help me compare my Bicep desired state with the deployed NSG state.
Reconcile the live environment
The preview gives you a safe description of the repair. Redeployment now makes the live network match the source-controlled definition.
Azure may take a few minutes to apply this deployment. A quiet terminal during that window does not signal a failure.
- Restore the declared network rule by running this deployment command in PowerShell:
az deployment group create --name deploy-cbt-status --resource-group rg-cbt-status-lab --parameters .\main.bicepparam
How does redeployment repair drift?
The deployment evaluates the declarations in main.bicep through main.bicepparam.
Because allow-ssh remains declared in the template, Azure recreates the missing rule. The source file drives recovery.
When the command finishes, the deployment output includes the sshCommand value for the restored administration path.
Deployment did not complete?
- Confirm PowerShell is using the folder that contains main.bicepparam.
- Confirm your Azure CLI session still uses the subscription containing rg-cbt-status-lab.
- Review the deployment output for the resource that Azure could not update.
For help with the failed deployment, help me diagnose why my Bicep redeployment did not restore allow-ssh.
Before you reconnect, do you expect the same SSH command that failed during the outage to work now?
- Locate the sshCommand value in the completed deployment output.
- Copy the full value of sshCommand.
- Switch back to Git Bash from earlier.
- Paste the copied value into the Git Bash prompt.
- Press Enter to run the copied SSH command.
You should see a remote shell prompt containing azureoperator@cbt-status-vm. That is the service restored through the declared state.
Good recovery. Your administration path works again because Bicep recreated the missing rule from main.bicep.
SSH still cannot connect?
- Confirm the Bicep deployment completed before starting the new SSH session.
- Confirm the copied sshCommand references ~/.ssh/id_rsa.pem.
- Confirm you ran the copied command in Git Bash where your private key remains available.
If the session still fails, help me trace why SSH remains unavailable after my Bicep deployment restored allow-ssh.
Record the incident
An incident report preserves the chain from symptom to evidence. It also shows how the declared state prevented a one-off repair from becoming the new normal.
- In the Visual Studio Code sidebar from earlier, create incident-report.md inside the open Bicep project.
- Add the incident summary and collected evidence by copying this first section into incident-report.md:
# SSH Connectivity Incident Report
## Summary
A new SSH administration session to `cbt-status-vm` failed after the expected inbound network security rule was removed.
## Detection
The previously successful SSH command could no longer establish a new connection to the VM public IP address.
## Evidence
### Network security group
The rule list for `cbt-status-nsg` did not contain `allow-ssh`.
### Azure Monitor Activity Log
The resource-group Activity Log contained the recent administrative change associated with the missing rule.
What does this evidence establish?
- The summary records the failed administration path.
- The detection section records the observable loss of connectivity.
- The evidence sections connect the absent rule with the recent control-plane change.
- Save incident-report.md.
- Confirm the editor shows the incident heading plus the Summary, Detection, and Evidence sections.
Report headings look wrong?
- Confirm each section heading begins with the same number of hash characters shown in the reference.
- Confirm blank lines separate each heading from its paragraph.
If the Markdown structure still looks incorrect, help me fix the heading structure in the first half of incident-report.md.
- In incident-report.md, place the cursor at the end of the Azure Monitor Activity Log sentence.
- Complete the diagnosis and recovery record by appending this section:
## Root Cause
The deployed Azure state drifted from `main.bicep` when the `allow-ssh` network security rule was deleted outside the Bicep deployment workflow.
## Recovery
A Bicep what-if operation predicted that the missing rule would be created. Redeploying `main.bicepparam` restored the rule, and a new SSH session succeeded.
## Prevention
- Preview infrastructure changes with a Bicep what-if operation.
- Treat `main.bicep` as the desired-state source of truth.
- Review Azure Monitor Activity Log during control-plane incidents.
- Use management locks for deletion protection while recognizing that a VM-scoped lock does not govern a separate network security group.
## Secret Mission Result
Record the blocked VM deletion attempt and the lock-related error here.
How does the report close the incident?
- The root cause names the drift between Azure and main.bicep.
- The recovery section records the prediction and redeployment result.
- The prevention section turns the incident into repeatable operating guidance.
- The final section leaves a defined place for the optional Secret Mission result.
- Save incident-report.md.
- Confirm the editor now shows the Root Cause, Recovery, Prevention, and Secret Mission Result sections.
Missing part of the recovery record?
- Confirm the second section appears below the Azure Monitor Activity Log evidence.
- Confirm all four prevention items remain formatted as separate Markdown list entries.
- Confirm the file name is exactly incident-report.md.
If your report differs from the reference, help me compare incident-report.md with the completed incident structure.
The comparison below is a final reference for the completed incident report.
✔️ Awesome, I've got everything!
Your report now connects the failed SSH session with the missing rule. It records how Bicep restored the declared state.
ⓧ I'd like to double check the full code
# SSH Connectivity Incident Report
## Summary
A new SSH administration session to `cbt-status-vm` failed after the expected inbound network security rule was removed.
## Detection
The previously successful SSH command could no longer establish a new connection to the VM public IP address.
## Evidence
### Network security group
The rule list for `cbt-status-nsg` did not contain `allow-ssh`.
### Azure Monitor Activity Log
The resource-group Activity Log contained the recent administrative change associated with the missing rule.
## Root Cause
The deployed Azure state drifted from `main.bicep` when the `allow-ssh` network security rule was deleted outside the Bicep deployment workflow.
## Recovery
A Bicep what-if operation predicted that the missing rule would be created. Redeploying `main.bicepparam` restored the rule, and a new SSH session succeeded.
## Prevention
- Preview infrastructure changes with a Bicep what-if operation.
- Treat `main.bicep` as the desired-state source of truth.
- Review Azure Monitor Activity Log during control-plane incidents.
- Use management locks for deletion protection while recognizing that a VM-scoped lock does not govern a separate network security group.
## Secret Mission Result
Record the blocked VM deletion attempt and the lock-related error here.
How to use this reference
Compare each heading and paragraph with your saved incident-report.md. The report should match this reference before you continue.
- Compare your saved incident-report.md with the complete reference.
- Confirm the report identifies main.bicep as the desired-state source of truth.
Your report now tells the complete incident story from detection through prevention. The recovered SSH session provides visible proof that the declared repair worked.
Secret mission
Prove the Deletion Lock Works
Test the VM's deletion protection with a real destructive request while keeping the lock intact. You will capture the blocked operation and explain why the separate NSG remained outside the VM-scoped safeguard.
Clean Up Your Resources
Clean Up Your Resources
Your Microsoft Azure lab can continue consuming credit while its resources remain deployed. Choose the cleanup option that matches what you want to do next.
Cost warning
The virtual machine and its supporting resources consume Azure credit while they remain deployed.
Free monthly quantities have limits. Introductory credit also has a limit.
Resources you used:
- The rg-cbt-status-lab resource group that contains the cloud lab.
- The cbt-status-vm Ubuntu virtual machine with its managed OS disk.
- The cbt-status-vnet virtual network with its management-subnet subnet.
- The cbt-status-nsg network security group with the restored allow-ssh rule.
- The cbt-status-pip public IP address.
- The cbt-status-nic network interface.
- The protect-cbt-status-vm VM-scoped management lock at the CanNotDelete level.
Keep everything running
No action needed. Choose this if you are still testing the live lab or practising the recovery workflow.
- Monitor your remaining Azure credit while the lab stays deployed.
- Keep cbt-status-vm available for SSH administration practice.
- Use the Bicep project as the source of truth for future infrastructure changes.
- Retain the protect-cbt-status-vm lock while the VM remains live.
Pause - I'll come back to this later
Deallocating the VM pauses its compute workload while preserving the lab configuration. The remaining Azure resources stay deployed.
- Deallocate cbt-status-vm from its virtual machine page in the Azure portal.
- Confirm Azure reports that cbt-status-vm is deallocated.
- Review current usage in Azure Cost Management to identify how much credit the remaining resources consume.
- Keep main.bicep, main.bicepparam, README.md, incident-report.md, and your local SSH key pair for the next session.
Delete - I don't want to use this again
Deleting the Azure resource group removes the live cloud lab. Your local Bicep files remain available if you decide to deploy it again.
Deletion removes the live lab
This cleanup removes the VM and every supporting resource in rg-cbt-status-lab. The deletion cannot be undone.
- Remove the VM management lock in PowerShell by running this command:
az resource lock delete --name protect-cbt-status-vm --resource-group rg-cbt-status-lab --resource cbt-status-vm --resource-type Microsoft.Compute/virtualMachines
What does this command do?
This command removes protect-cbt-status-vm from cbt-status-vm. Removing the lock allows Azure to delete the resource group.
- Confirm the command returns to PowerShell without an error.
Could not remove the lock?
- Check that you are still signed in to the subscription containing rg-cbt-status-lab.
- Check that the lock name is protect-cbt-status-vm.
- Help me troubleshoot why Azure did not remove the VM management lock.
- Delete the resource group and every resource inside it by running this command:
az group delete --name rg-cbt-status-lab --yes
What does this command do?
This command deletes rg-cbt-status-lab with all of its contained resources. The --yes option suppresses the confirmation prompt.
This deletion can take a few minutes while Azure removes the network and compute resources.
- Wait for PowerShell to return to the prompt.
Resource group deletion blocked?
- Confirm the VM management lock removal completed successfully.
- Check that your active subscription contains rg-cbt-status-lab.
- Help me troubleshoot why Azure did not delete the resource group.
Before you check, do you expect Azure to find the resource group after the deletion completes?
- Verify that the resource group is gone by running this command:
az group exists --name rg-cbt-status-lab
What does this command do?
This command checks whether Azure can still find rg-cbt-status-lab in the active subscription.
Good work. You will see false, which confirms the cloud lab has been removed.
Still seeing true?
- Wait for the resource-group deletion operation to finish.
- Run the verification command again after PowerShell returns to the prompt.
- Help me check why the Azure resource group still exists.
Nice Work!
Nice Work!
You did it! You built a governed Linux operations lab on Microsoft Azure with Bicep.
You've learned how to:
- Built a reusable cloud lab from Bicep. Deployed an Ubuntu virtual machine inside a dedicated virtual network. Opened a controlled SSH administration session with public-key authentication.
- Applied governance tags to every resource created directly by the template. Protected cbt-status-vm with a VM-scoped CanNotDelete management lock.
- Created configuration drift by removing allow-ssh. Traced the outage through the network security group rule list. Confirmed the control-plane change in Azure Monitor Activity Log. Restored SSH access from main.bicep. Recorded the evidence in incident-report.md.
- Secret Mission: Proved the VM-scoped CanNotDelete lock blocks deletion of cbt-status-vm. Documented why that lock does not govern the separate cbt-status-nsg resource.
Ready to quiz yourself?