Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
33e16b5
Automate hosted LocalBox preparation and readiness checks
janegilring Nov 2, 2026
6c2e4ab
Fix LocalBox guest packaging for Console content mounts
janegilring Nov 2, 2026
810f70c
Provision kubectl for unattended LocalBox health checks
janegilring Nov 2, 2026
6da1e03
Pin and test the SYSTEM-compatible LocalBox guest runner
janegilring Nov 2, 2026
948c71f
Route LocalBox AKS health checks through the internal gateway
janegilring Nov 3, 2026
f9e966e
Pin and test temporary LocalBox AKS health routing
janegilring Nov 3, 2026
33f8f55
Show verified LocalBox test summaries in Console output
janegilring Nov 3, 2026
002c287
Fix Console parsing of LocalBox result downloads
janegilring Nov 4, 2026
e738a3d
Document LocalBox test-06 deployment timings and optimization options
janegilring Nov 4, 2026
212ff97
Use Developer Bastion for new LocalBox deployments
janegilring Nov 4, 2026
2f51015
Retry transient Azure Local validation download timeouts once
janegilring Nov 4, 2026
853c4e0
Surface actionable LocalBox guest failure diagnostics in Console
janegilring Nov 5, 2026
a418838
Recover missing MetalLB runtime objects with one guarded reapply
janegilring Nov 5, 2026
2904865
Pin hosted LocalBox guest package with bounded MetalLB recovery
janegilring Nov 5, 2026
2c7fad3
Overlap fresh AKS provisioning with LocalBox VM image import
janegilring Nov 5, 2026
ada1bfb
Pin guest package with overlapping AKS and image provisioning
janegilring Nov 5, 2026
bfb3d17
Guard new LocalBox startup downloads and surface early failures
janegilring Nov 5, 2026
81cd5c6
Wait for LocalBox prerequisites and preserve startup identity
janegilring Nov 6, 2026
73241c1
Preserve upstream LocalBox error semantics at startup boundary
janegilring Nov 6, 2026
b7db4af
Harden LocalBox prerequisites and validation recovery
janegilring Nov 7, 2026
32397bf
Reconcile accepted AKS deployments after no-wait visibility errors
janegilring Nov 7, 2026
5d785ab
Pin verified AKS submission reconciliation guest sources
janegilring Nov 7, 2026
4fdcfe2
Correct live aksarc version evidence and record image handoff
janegilring Nov 7, 2026
cf30f14
Synchronize guest pin after aksarc evidence correction
janegilring Nov 7, 2026
ceed77b
Record TEST12 teardown and remaining Full validation gap
janegilring Nov 7, 2026
238925b
Gate LocalBox preparation on Blob write readiness
janegilring Nov 7, 2026
c4e935c
Pin the Blob write-readiness guest fix
janegilring Nov 7, 2026
ca25506
Record TEST13 passing Blob readiness after recovery
janegilring Nov 7, 2026
46fa053
Record verified TEST13 Full health and MetalLB recovery
janegilring Nov 7, 2026
8f284c0
Fix BGInfo discovery and process-aware startup monitoring
janegilring Nov 8, 2026
f8e09ee
Document TEST14 Full health, section timings and recovered warnings
janegilring Nov 8, 2026
0048725
Add bounded participant K3s installation recovery
janegilring Nov 8, 2026
b21ac0b
Check BGInfo process exit instead of stale PowerShell status
janegilring Nov 8, 2026
e1b47fa
Account for existing participant compute when retrying quota checks
janegilring Nov 9, 2026
3d79d79
Document targeted K3s extension recovery for event leads
janegilring Nov 9, 2026
7f47eee
docs: align Sovereign Cloud guides with automated LocalBox readiness
janegilring Nov 9, 2026
48a07d3
Merge branch 'microsoft:main' into feature/sovereign-localbox-automation
janegilring Nov 9, 2026
cc12576
fix: recover safely from asynchronous LocalBox capacity failures
janegilring Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions 03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,24 @@ This Hackathon has a few but important prerequisites
In order to use the Hackathon time most effectively, the following tasks should be completed prior to starting the session.

> [!NOTE]
> For eligible Microsoft-hosted events, use the [Hacks Console runbook](https://github.com/microsoft/MicroHack/blob/main/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/hosted-events/readme.md). Outside those events, use [manual infrastructure setup](https://github.com/microsoft/MicroHack/blob/main/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/manual-setup/readme.md). Both paths require LocalBox preparation and health validation before participants begin.
> For eligible Microsoft-hosted events, use the [Hacks Console runbook](resources/hosted-events/readme.md). Console automatically deploys/reuses LocalBox, prepares its shared resources and verifies infrastructure readiness before shared and participant deployments. No routine Client login or manual preparation is required. Event leads still verify participant access and Defender cloud reporting. Outside those events, use [manual infrastructure setup](resources/manual-setup/readme.md), then run preparation and health checks explicitly.

1. For hosted events, the hacker account and assigned Azure subscription/resource group from the Hackathon Console; otherwise, your own Azure subscription with Owner RBAC rights at the subscription level
2. The resource, policy and role-assignment permissions specified by each challenge in your assigned scope
3. Access to Azure Arc Jumpstart LocalBox if completing Challenge 6, including the [guest-management and Defender readiness checks](https://github.com/microsoft/MicroHack/blob/main/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/demo-vm-creator/README.md#step-6-test-the-environment)
3. Access to the prepared Azure Arc Jumpstart LocalBox environment if completing Challenge 6; the organizer must complete the [participant-access, guest-management and Defender readiness checks](resources/localbox/manual-preparation.md#step-6-test-the-environment). Participants still create their own Windows VM and challenge workloads.
4. The [recommended Codespaces environment](#recommended-environment-github-codespaces), or a local workstation with [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) and the tools listed in each walkthrough
5. Challenge 7: `kubectl`, `jq`, OpenSSL, the [Radius CLI](https://docs.radapp.io/getting-started/install/), and the Azure CLI `bastion` extension (included in the Sovereign Cloud devcontainer)

The shared Challenge 5/7 AKS platform requires **4 AMD SEV-SNP confidential-family vCPUs per participant** for two Ubuntu nodes, plus **12 DSv5 vCPUs** for two system nodes and one K3s VM: **16 regional vCPUs** in total. There is no standalone Confidential VM. Automation evaluates `Standard_DC2as_v5` and `Standard_DC2as_v6` across the configured regions. With two labs per subscription, allow 8 confidential-family, 24 DSv5 and 32 regional vCPUs before deployment. These are steady-state requirements; also reserve upgrade surge headroom (for example one additional node per pool: 2 confidential plus 4 DSv5 vCPUs per concurrent cluster upgrade).

The default candidate order is Sweden Central/v5, Spain Central/v5, Sweden Central/v6, then Spain Central/v6. Shared preparation persists the first candidate with supported VM sizes and sufficient confidential-family, DSv5-family, and regional quota so every participant deployment in that subscription uses the same selection.
The default candidate order is Spain Central/v5, Sweden Central/v5, Spain Central/v6, then Sweden Central/v6. Shared preparation persists the first candidate with supported VM sizes and sufficient confidential-family, DSv5-family, and regional quota so every participant deployment in that subscription uses the same selection. Retries preserve the saved selection and credit confirmed existing allocations rather than requiring the full lab capacity again as unused quota.

On 2026-09-21, read-only `az vm list-skus --all` checks in the validation subscription returned `Standard_DC2as_v6` in **both Sweden Central and Spain Central**, with no restrictions; `Standard_DC2as_v5` was not listed in either. This is a dated subscription-specific observation, not a capacity guarantee or proof of AKS deployment success. Recheck SKUs, quota and AKS support for each event. Ubuntu CVM pools are supported as described in [AKS confidential VM guidance](https://learn.microsoft.com/azure/aks/use-cvm).

Challenge 4 independently deploys ACI/ACR in **North Europe**, the walkthrough's validated Confidential ACI region. It does not use VM-family quota, the old standalone CVM, or a custom attestation provider. Confirm Confidential ACI quota/capacity separately; VM availability in Sweden/Spain does not establish Confidential ACI availability there. Shared automation registers `Microsoft.ContainerInstance` and `Microsoft.ContainerRegistry`. Challenge 5 uses the actual Console AKS region, not Challenge 4's `LOCATION` value.

Event-specific capacity fallback can place participant AKS/K3s resources outside the default regions, including **West US 2**. This is a synthetic-lab workaround, **not an EU data-residency recommendation**. Neither Spain Central nor West US 2 is in Challenge 1's example location allowlist. Keep exercise assignments in **DoNotEnforce** except for the explicitly scoped enforcement test, then restore that setting; inherited policies still apply. The Azure Local/custom-location registration defaults to **Australia East**, independently of the LocalBox host and participant AKS regions. See the [regional policy and retry guidance](resources/hosted-events/readme.md#participant-regions-and-exercise-policies) before changing an event's locations.

```powershell
./resources/manual-setup/subscription-preparations/2-vcpu-quotas.ps1 -Region swedencentral -NumberOfLabUsers 2 -ConfidentialVmGeneration v5 -SubmitQuotaRequests
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,23 @@ if($PreferredLocation.Count -gt 0) {
$deploymentLocations = @('swedencentral')
}

$selection = Get-MhhConfidentialComputeSelection -SubscriptionId $SubscriptionId
$validCandidates = @(Get-MhhConfidentialComputeCandidates -PreferredLocation $deploymentLocations)
$selectedCandidate = $validCandidates |
Where-Object {
$_.Location -ieq $selection.Location -and
$_.VmSize -ieq $selection.VmSize -and
$_.QuotaName -ieq $selection.QuotaName
} |
Select-Object -First 1
$existingCompute = Get-MhhSovereignExistingCompute -SubscriptionId $SubscriptionId `
-NameSuffixes @($stableHash.Substring(0, 8)) -ResourceGroupName $ResourceGroupName
$selectedCandidate = Get-MhhSovereignResumeCandidate -SubscriptionId $SubscriptionId -ExistingCompute $existingCompute
if(-not $selectedCandidate) {
throw 'The shared subscription preparation did not persist a valid confidential compute selection for this deployment.'
}

$confidentialVmSize = $selectedCandidate.VmSize
$confidentialQuotaName = $selectedCandidate.QuotaName
$deploymentLocations = @($selectedCandidate.Location)
$quotaRequirements = @(Get-MhhSovereignComputeRequirements -ConfidentialQuotaName $confidentialQuotaName)
$quotaRequirements = @(Get-MhhSovereignAdditionalCompute -Candidate $selectedCandidate -ExistingCompute $existingCompute)
$regionReadinessSummary = @()
foreach($candidateLocation in $deploymentLocations) {
$requiredVmSizes = @($confidentialVmSize, 'Standard_D4s_v5')
$requiredVmSizes = @(
if (($quotaRequirements | Where-Object Name -eq $confidentialQuotaName).Additional -gt 0) { $confidentialVmSize }
if (($quotaRequirements | Where-Object Name -eq 'StandardDSv5Family').Additional -gt 0) { 'Standard_D4s_v5' }
)
$availableVmSizes = Get-AzComputeResourceSku -Location $candidateLocation -ErrorAction Stop |
Where-Object {
$_.ResourceType -ieq 'virtualMachines' -and
Expand All @@ -84,8 +81,7 @@ foreach($candidateLocation in $deploymentLocations) {
$unavailableVmSizes = @($requiredVmSizes | Where-Object { $_ -inotin $availableVmSizes })

if($unavailableVmSizes.Count -gt 0) {
$regionReadinessSummary += "${candidateLocation}: unavailable VM sizes $($unavailableVmSizes -join ', ')"
continue
throw "${candidateLocation}: unavailable VM sizes $($unavailableVmSizes -join ', ') for missing participant compute."
}

$regionalUsage = Get-AzVMUsage -Location $candidateLocation -ErrorAction Stop
Expand All @@ -96,10 +92,10 @@ foreach($candidateLocation in $deploymentLocations) {
$current = if($null -eq $quota) { 0 } else { $quota.CurrentValue }
$limit = if($null -eq $quota) { 0 } else { $quota.Limit }
$available = $limit - $current
$regionReadinessSummary += "${candidateLocation}/$($requirement.Name): ${current}/${limit} vCPUs used"
$regionReadinessSummary += "${candidateLocation}/$($requirement.Name): ${current}/${limit} vCPUs used; $($requirement.Existing) already allocated to this lab; $($requirement.Additional) additional required"

if($available -lt $requirement.PerParticipant) {
throw "The shared compute selection is no longer ready ($($regionReadinessSummary -join '; ')). $($requirement.PerParticipant) available $($requirement.Name) vCPUs are required per participant."
if($requirement.Additional -gt 0 -and $available -lt $requirement.Additional) {
throw "The shared compute selection is no longer ready ($($regionReadinessSummary -join '; ')). $($requirement.Additional) additional $($requirement.Name) vCPUs are required for this participant."
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
.SYNOPSIS
Deploys the Azure Arc Jumpstart LocalBox environment used by the Sovereign Cloud lab.
.DESCRIPTION
Downloads a coherent set of upstream LocalBox Bicep templates, validates them, and
deploys the LocalBox host, networking, storage, and optional Azure Bastion resources.
Downloads upstream LocalBox Bicep templates and substitutes the lab's Developer
Bastion network module, then validates and deploys the environment.
.PARAMETER SubscriptionId
The Azure subscription that receives the LocalBox resources.
.PARAMETER ResourceGroupName
Expand All @@ -21,6 +21,9 @@ It is not published as a participant credential. Coach/event-lead-only retrieval
requires a supported mechanism from the Console owner.
.PARAMETER GithubRef
The azure_arc branch, tag, or commit used for both Bicep and runtime artifacts.
.PARAMETER DeployBastion
Deploy Bastion Developer for one browser-based coach connection at a time.
The host remains private and retains NAT Gateway outbound access when enabled.
.PARAMETER AzureLocalResourceProviderObjectId
Optional tenant-specific object ID of the Microsoft.AzureStackHCI enterprise
application. The script resolves it through Microsoft Graph when omitted.
Expand Down Expand Up @@ -105,6 +108,34 @@ function New-LocalBoxPassword {
return -join ($characters | Sort-Object { Get-Random })
}

function Assert-LocalBoxNetworkCompatibility {
param([string]$SubscriptionId, [string]$ResourceGroupName)

$bastions = @(Invoke-AzJson -Arguments @(
'network', 'bastion', 'list', '--subscription', $SubscriptionId, '--resource-group', $ResourceGroupName
))
foreach ($bastion in @($bastions | Where-Object name -eq 'LocalBox-Bastion')) {
if ($bastion.sku.name -ne 'Developer') {
throw "Existing LocalBox-Bastion SKU '$($bastion.sku.name)' cannot be changed to Developer in place. Use a fresh resource group or arrange an explicitly approved delete/recreate migration. No existing Bastion was changed."
}
}
$networks = @(Invoke-AzJson -Arguments @(
'network', 'vnet', 'list', '--subscription', $SubscriptionId, '--resource-group', $ResourceGroupName
))
$legacySubnets = @($networks | Where-Object name -eq 'LocalBox-VNet' |
ForEach-Object { $_.subnets } | Where-Object name -eq 'AzureBastionSubnet')
if ($legacySubnets.Count) {
throw 'LocalBox-VNet contains a legacy AzureBastionSubnet. Use a fresh resource group or arrange an explicitly approved migration; this deployment will not automatically remove an existing Bastion subnet.'
}
}

$networkTemplateFile = Join-Path $PSScriptRoot 'localbox-network.bicep'
if (-not (Test-Path -LiteralPath $networkTemplateFile -PathType Leaf)) {
throw 'Missing localbox-network.bicep. Import the complete labautomation directory before deploying LocalBox.'
}
$startupSource = Join-Path $PSScriptRoot 'localbox-startup.ps1'
. $startupSource

if (-not (Get-Command az -ErrorAction SilentlyContinue)) {
throw 'Azure CLI is required to deploy LocalBox.'
}
Expand Down Expand Up @@ -194,27 +225,27 @@ Invoke-AzJson -Arguments @(
'--namespace', 'Microsoft.Network', '--wait'
) | Out-Null

$availableVmSize = & az vm list-sizes `
--subscription $SubscriptionId `
--location $Location `
--query "[?name=='$VmSize'].name | [0]" `
--output tsv `
--only-show-errors
if ($LASTEXITCODE -ne 0) {
throw "Unable to query VM size availability in $Location."
}
if ($availableVmSize -ne $VmSize) {
throw "VM size $VmSize is unavailable in $Location."
$availableVmSizes = @(Invoke-AzJson -Arguments @(
'vm', 'list-skus', '--subscription', $SubscriptionId, '--location', $Location,
'--resource-type', 'virtualMachines', '--size', $VmSize, '--all'
) | Where-Object { $_.name -eq $VmSize })
if ($availableVmSizes.Count -ne 1 -or
@($availableVmSizes[0].restrictions | Where-Object { $_.type -ne 'Zone' }).Count -gt 0) {
throw "VM size $VmSize is unavailable in $Location for this subscription (missing SKU or regional restriction). No deployment was submitted."
}

$familyName = if ($VmSize -eq 'Standard_E32s_v6') { 'Standard Esv6 Family vCPUs' } else { 'Standard Esv5 Family vCPUs' }
$familyName = if ($VmSize -eq 'Standard_E32s_v6') { 'standardEsv6Family' } else { 'standardESv5Family' }
$usageEntries = Invoke-AzJson -Arguments @(
'vm', 'list-usage', '--subscription', $SubscriptionId, '--location', $Location
)
$usage = @($usageEntries | Where-Object { $_.name.localizedValue -eq $familyName }) | Select-Object -First 1
$usage = @($usageEntries | Where-Object { $_.name.value -eq $familyName }) | Select-Object -First 1
if (-not $usage -or ($usage.limit - $usage.currentValue) -lt 32) {
throw "At least 32 unused $familyName vCPUs are required in $Location."
}
$regionalUsage = @($usageEntries | Where-Object { $_.name.value -eq 'cores' }) | Select-Object -First 1
if (-not $regionalUsage -or ($regionalUsage.limit - $regionalUsage.currentValue) -lt 32) {
throw "At least 32 unused regional vCPUs are required in $Location."
}

if ([string]::IsNullOrWhiteSpace($AzureLocalResourceProviderObjectId)) {
$servicePrincipals = Invoke-AzJson -Arguments @(
Expand Down Expand Up @@ -254,6 +285,7 @@ if ($LASTEXITCODE -ne 0) {
}

if ($resourceGroupExists -eq 'true') {
Assert-LocalBoxNetworkCompatibility -SubscriptionId $SubscriptionId -ResourceGroupName $ResourceGroupName
$resourceGroup = Invoke-AzJson -Arguments @(
'group', 'show', '--subscription', $SubscriptionId, '--name', $ResourceGroupName
)
Expand All @@ -279,7 +311,6 @@ $bicepFiles = @(
'mgmt/mgmtArtifacts.bicep',
'mgmt/storageAccount.bicep',
'mgmt/customerUsageAttribution.bicep',
'network/network.bicep',
'host/host.bicep'
)
$deploymentName = "localbox-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
Expand All @@ -292,12 +323,46 @@ try {
Write-Host "Downloading LocalBox template: $file"
Invoke-WebRequest -Uri "$baseUri/$file" -OutFile (Join-Path $tempDirectory $file) -UseBasicParsing
}
Copy-Item -LiteralPath $networkTemplateFile -Destination (Join-Path $tempDirectory 'network/network.bicep') -ErrorAction Stop
if ($DeployBastion) {
Write-Host 'Using Bastion Developer: one browser connection at a time; no native-client tunnels. Regional support is checked by ARM validation.' -ForegroundColor Cyan
}

$resourceTags = @{
Project = 'jumpstart_LocalBox'
SecurityControl = 'Ignore'
CostControl = 'Ignore'
}
$clients = @(Invoke-AzJson -Arguments @('resource', 'list', '--subscription', $SubscriptionId,
'--resource-group', $ResourceGroupName, '--resource-type', 'Microsoft.Compute/virtualMachines') |
Where-Object name -eq 'LocalBox-Client')
if ($clients.Count -gt 1) { throw 'Cannot uniquely identify the existing LocalBox Client.' }
$newClient = $clients.Count -eq 0
$startupId = [guid]::Empty
if ($newClient) { $startupId = [guid]::NewGuid() }
elseif ($clients[0].tags.'microhack-startup-version' -or $clients[0].tags.'microhack-startup-id') {
if ($clients[0].tags.'microhack-startup-version' -ne '1' -or
-not [guid]::TryParse([string]$clients[0].tags.'microhack-startup-id', [ref]$startupId) -or $startupId -eq [guid]::Empty) {
throw 'Existing LocalBox startup-protection metadata is unrecognized. No bootstrap migration was attempted.'
}
}
if ($startupId -ne [guid]::Empty) {
$resourceTags['microhack-startup-version'] = '1'
$resourceTags['microhack-startup-id'] = $startupId.ToString()
Set-LocalBoxStartupTemplate -HostTemplate (Join-Path $tempDirectory 'host/host.bicep') `
-StartupSource $startupSource -Id $startupId -NewVm:$newClient
Write-Host 'Using correlated startup protection with bounded base-image downloads. Existing VM custom data is never replaced.'
}
else {
$existingMonitors = @(Invoke-AzJson -Arguments @(
'vm', 'run-command', 'list', '--subscription', $SubscriptionId,
'--resource-group', $ResourceGroupName, '--vm-name', 'LocalBox-Client'
) | Where-Object name -eq 'sovereign-localbox-startup-watch')
if ($existingMonitors.Count) {
throw 'The existing Client has a startup monitor but no startup identity tags. Organizer review is required; no bootstrap migration was attempted.'
}
Write-Warning 'This existing Client predates startup protection. Its bootstrap is left unchanged; use a new VM to enable guarded startup.'
}

@{
'$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#'
Expand Down Expand Up @@ -332,6 +397,14 @@ try {
'--template-file', $templateFile, '--parameters', "@$parametersFile"
) | Out-Null

if ($startupId -ne [guid]::Empty) {
# Upstream progress updates replace VM tags with the resource group's tags.
Invoke-AzJson -Arguments @(
'group', 'update', '--subscription', $SubscriptionId, '--name', $ResourceGroupName,
'--set', 'tags.microhack-startup-version=1', "tags.microhack-startup-id=$startupId"
) | Out-Null
}

Write-Host "Starting LocalBox deployment '$deploymentName'..." -ForegroundColor Cyan
$deploymentArguments = @(
'deployment', 'group', 'create', '--subscription', $SubscriptionId,
Expand Down
Loading
Loading