diff --git a/aci-samples/visual-attestation-demo-v2/Deploy-VisualAttestationV2.ps1 b/aci-samples/visual-attestation-demo-v2/Deploy-VisualAttestationV2.ps1
index dfaabf1..e2d2252 100644
--- a/aci-samples/visual-attestation-demo-v2/Deploy-VisualAttestationV2.ps1
+++ b/aci-samples/visual-attestation-demo-v2/Deploy-VisualAttestationV2.ps1
@@ -231,6 +231,156 @@ function Wait-ForContainer {
return $false
}
+function New-ComparePage {
+ param(
+ [string]$ConfidentialUrl,
+ [string]$StandardUrl,
+ [string]$OutputPath
+ )
+
+ $html = @"
+
+
+
+
+
+ ACI Attestation Compare
+
+
+
+
+
+
+
+
+
Confidential
+
+ SEV-SNP expected
+ $ConfidentialUrl
+
+
+
+
+
+
+
+
Standard
+
+ Expected /dev/sev-guest missing
+ $StandardUrl
+
+
+
+
+
+
+
+"@
+
+ Set-Content -Path $OutputPath -Value $html -Encoding UTF8
+}
+
# ============================================================================
# Deploy phase (single container group)
# ============================================================================
@@ -362,15 +512,38 @@ function Invoke-Compare {
Wait-ForContainer -Fqdn $fqdn_std | Out-Null
Write-Header "Both containers deployed"
- Write-Host "Confidential (attestation succeeds): http://$fqdn_conf"
- Write-Host "Standard (attestation FAILS) : http://$fqdn_std"
+
+ # Retrieve SKU info
+ $confSku = az container show -g $cfg.resourceGroup -n $name_conf --query "sku" -o tsv
+ $stdSku = az container show -g $cfg.resourceGroup -n $name_std --query "sku" -o tsv
+
+ # Output summary table
+ Write-Host ""
+ Write-Host "╔════════════════════════════════════════════════════════════════╗"
+ Write-Host "║ Compare Deployment Summary ║"
+ Write-Host "╠════════════════════════════════════════════════════════════════╣"
+ Write-Host "║ Confidential SKU (Attestation succeeds) ║"
+ Write-Host "║ SKU: $($confSku.PadRight(54)) ║"
+ Write-Host "║ URL: http://$($fqdn_conf.PadRight(49)) ║"
+ Write-Host "╠════════════════════════════════════════════════════════════════╣"
+ Write-Host "║ Standard SKU (Attestation FAILS) ║"
+ Write-Host "║ SKU: $($stdSku.PadRight(54)) ║"
+ Write-Host "║ URL: http://$($fqdn_std.PadRight(49)) ║"
+ Write-Host "╚════════════════════════════════════════════════════════════════╝"
+ Write-Host ""
+ Write-Host "Click 'Attest' on each pane to see the difference:"
+ Write-Host " • Confidential: Hardware-rooted SEV-SNP attestation succeeds"
+ Write-Host " • Standard: Attestation fails (no /dev/sev-guest)"
Write-Host ""
- Write-Host "Open both side-by-side, click 'Run Attestation' on each, and compare."
if (-not $SkipBrowser) {
- Start-Process "http://$fqdn_conf"
- Start-Sleep -Seconds 1
- Start-Process "http://$fqdn_std"
+ $confUrl = "http://$fqdn_conf"
+ $stdUrl = "http://$fqdn_std"
+ $comparePage = Join-Path $PSScriptRoot 'side-by-side-compare.html'
+ New-ComparePage -ConfidentialUrl $confUrl -StandardUrl $stdUrl -OutputPath $comparePage
+ Write-Success "Generated: $comparePage"
+ Write-Host "Opening in new browser window..."
+ Start-Process $comparePage
}
}
diff --git a/aci-samples/visual-attestation-demo-v2/README.md b/aci-samples/visual-attestation-demo-v2/README.md
index 79c931e..9a258a5 100644
--- a/aci-samples/visual-attestation-demo-v2/README.md
+++ b/aci-samples/visual-attestation-demo-v2/README.md
@@ -134,8 +134,9 @@ prints the last 25 lines of container logs for inspection.
./Deploy-VisualAttestationV2.ps1 -Compare
```
-Deploys both flavors in the same resource group and opens both URLs. Same
-image, same code path, opposite result - the cleanest way to demo why
+Deploys both flavors in the same resource group and opens a generated
+`side-by-side-compare.html` page that embeds both live endpoints in one view.
+Same image, same code path, opposite result - the cleanest way to demo why
attestation matters.
### 5. Cleanup
@@ -172,6 +173,14 @@ contrast - it proves the success case really did need confidential hardware.

+### Side-by-side compare view
+
+The compare page below is generated automatically by
+`Deploy-VisualAttestationV2.ps1 -Compare` and opens both live deployments in
+one browser window.
+
+
+
## Why two SKUs?
The point of this sample is **falsifiability**. A demo that only ever shows
diff --git a/aci-samples/visual-attestation-demo-v2/images/screenshot-side-by-side.png b/aci-samples/visual-attestation-demo-v2/images/screenshot-side-by-side.png
new file mode 100644
index 0000000..4e8562a
Binary files /dev/null and b/aci-samples/visual-attestation-demo-v2/images/screenshot-side-by-side.png differ
diff --git a/aci-samples/visual-attestation-demo-v2/side-by-side-compare.html b/aci-samples/visual-attestation-demo-v2/side-by-side-compare.html
new file mode 100644
index 0000000..c190215
--- /dev/null
+++ b/aci-samples/visual-attestation-demo-v2/side-by-side-compare.html
@@ -0,0 +1,137 @@
+
+
+
+
+
+ ACI Attestation Compare
+
+
+
+
+
+
+
+
+
Confidential
+
+ SEV-SNP expected
+ http://cc-attest-conf-07131653.eastus.azurecontainer.io
+
+
+
+
+
+
+
+
Standard
+
+ Expected /dev/sev-guest missing
+ http://cc-attest-std-07131655vspm.eastus.azurecontainer.io
+
+
+
+
+
+
+
diff --git a/aks-samples/virtual nodes/Deploy-VirtualNodesAKS.ps1 b/aks-samples/virtual nodes/Deploy-VirtualNodesAKS.ps1
index 9ea5488..84928b3 100644
--- a/aks-samples/virtual nodes/Deploy-VirtualNodesAKS.ps1
+++ b/aks-samples/virtual nodes/Deploy-VirtualNodesAKS.ps1
@@ -862,6 +862,7 @@ Invoke-Az "az network vnet subnet create --resource-group $resourceGroupName --v
$aksSubnetId = (Invoke-Az "az network vnet subnet show --resource-group $resourceGroupName --vnet-name $vnetName --name $aksSubnetName --query id --output tsv").Trim()
$aciSubnetId = (Invoke-Az "az network vnet subnet show --resource-group $resourceGroupName --vnet-name $vnetName --name $aciSubnetName --query id --output tsv").Trim()
+$aciSubnetNsgId = (Invoke-Az "az network vnet subnet show --resource-group $resourceGroupName --vnet-name $vnetName --name $aciSubnetName --query networkSecurityGroup.id --output tsv").Trim()
$vnetId = (Invoke-Az "az network vnet show --resource-group $resourceGroupName --name $vnetName --query id --output tsv").Trim()
Write-Header "Creating AKS cluster"
@@ -874,6 +875,9 @@ Invoke-Az @"
az aks nodepool add --resource-group $resourceGroupName --cluster-name $aksName --name $confidentialPoolName --node-count $ConfidentialNodeCount --node-vm-size $ConfidentialVmSize --os-sku Ubuntu --mode User --labels workload=confidential sku=amd-sev-snp --tags owner=$ownerUpn BuiltBy=$scriptName Workload=virtual-nodes CCType=AMD-SEV-SNP --only-show-errors
"@
+Write-Header "Waiting for AKS operation to complete"
+Invoke-Az "az aks wait --resource-group $resourceGroupName --name $aksName --updated --interval 15 --timeout 1800"
+
Write-Header "Enabling virtual nodes"
Invoke-Az "az aks enable-addons --resource-group $resourceGroupName --name $aksName --addons virtual-node --subnet-name $aciSubnetName --only-show-errors"
@@ -884,6 +888,9 @@ $aciConnectorPrincipalId = Wait-ForManagedIdentity -ResourceGroup $nodeResourceG
Write-Header "Assigning subnet rights to the virtual nodes identity"
Ensure-RoleAssignment -AssigneeObjectId $aciConnectorPrincipalId -RoleName "Network Contributor" -Scope $aciSubnetId
Ensure-RoleAssignment -AssigneeObjectId $aciConnectorPrincipalId -RoleName "Network Contributor" -Scope $vnetId
+if ($aciSubnetNsgId) {
+ Ensure-RoleAssignment -AssigneeObjectId $aciConnectorPrincipalId -RoleName "Network Contributor" -Scope $aciSubnetNsgId
+}
Write-Header "Connecting kubectl"
Invoke-Az "az aks get-credentials --resource-group $resourceGroupName --name $aksName --overwrite-existing --only-show-errors"
diff --git a/aks-samples/virtual nodes/README.md b/aks-samples/virtual nodes/README.md
index 630e88e..73e23d9 100644
--- a/aks-samples/virtual nodes/README.md
+++ b/aks-samples/virtual nodes/README.md
@@ -21,6 +21,78 @@ This is useful when you want rapid burst capacity, simpler operations for specif
- Isolating selected workloads from regular AKS node pools while keeping a single Kubernetes API surface
- Hybrid AKS + ACI patterns where most services run on AKS nodes and overflow or special workloads run on virtual nodes
+## Logical architecture
+
+```mermaid
+flowchart LR
+ U[User Browser] --> LB[Public LoadBalancer Service]
+ LB --> APISVC[Kubernetes Service in AKS]
+
+ subgraph AKS[AKS Cluster]
+ SYSP[System pool VMSS]
+ CCP[Confidential pool VMSS]
+ VN[virtual-node-aci-linux]
+ APISVC --> VN
+ end
+
+ subgraph VNET[Virtual Network]
+ AKSSN[aks-subnet]
+ ACISN[aci-subnet]
+ NSG[ACI subnet NSG]
+ end
+
+ VN --> ACISN
+ ACISN --> ACI[ACI container group backing virtual-node pod]
+ NSG -. enforced on .- ACISN
+
+ MI[aciConnector managed identity]
+ MI -. Network Contributor .-> ACISN
+ MI -. Network Contributor .-> VNET
+ MI -. Network Contributor .-> NSG
+```
+
+## Deployment sequence
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Script as Deploy-VirtualNodesAKS.ps1
+ participant Azure as Azure Control Plane
+ participant AKS as AKS Cluster
+ participant MI as aciConnector Identity
+ participant ACI as ACI Virtual Node Backend
+
+ User->>Script: Run deployment script
+ Script->>Azure: Create RG, VNet, aks-subnet, aci-subnet
+ Script->>Azure: Create AKS cluster
+ Script->>Azure: Add confidential node pool
+ Script->>Azure: Wait for AKS update to finish
+ Script->>Azure: Enable virtual-node addon
+ Azure-->>MI: Create aciConnector managed identity
+ Script->>Azure: Assign Network Contributor on subnet, VNet, subnet NSG
+ Script->>AKS: Restart aci-connector pod
+ ACI-->>AKS: Register virtual-node-aci-linux
+ Script->>AKS: Apply workload manifest
+ AKS->>ACI: Schedule virtual-node pod as ACI container group
+```
+
+## Runtime sequence
+
+```mermaid
+sequenceDiagram
+ participant Browser
+ participant LB as Public LoadBalancer
+ participant K8s as Kubernetes Service
+ participant VN as virtual-node-aci-linux
+ participant CG as ACI Container Group
+
+ Browser->>LB: HTTP request
+ LB->>K8s: Forward to service
+ K8s->>VN: Route to selected virtual-node pod
+ VN->>CG: Deliver traffic to backing ACI container
+ CG-->>Browser: HTTP response
+```
+
### Official documentation
- AKS Virtual Nodes overview: https://learn.microsoft.com/azure/aks/virtual-nodes
@@ -57,7 +129,9 @@ The script requires an active subscription-scope role that can both create resou
If one of those roles is only eligible through Microsoft Entra PIM, the script pauses and asks you to activate it before retrying the permission check.
-During live validation, the AKS virtual-nodes connector identity needed `Network Contributor` on both the `aci-subnet` and the parent virtual network to stabilize quickly, so the script assigns both scopes before restarting the connector.
+During live validation, the AKS virtual-nodes connector identity needed `Network Contributor` on the `aci-subnet`, the parent virtual network, and the subnet NSG that AKS links to the ACI subnet. Without the NSG scope, the connector can fail with `LinkedAuthorizationFailed` on `Microsoft.Network/networkSecurityGroups/join/action`.
+
+The script now assigns all three scopes before restarting the connector.
## Usage
@@ -88,6 +162,10 @@ This first sample validates the virtual-nodes plumbing on top of a confidential-
That gives you a working foundation for the next step: replacing the sample manifest with a confidential ACI deployment that includes a generated Confidential Computing Enforcement policy and any attestation sidecars you want to test.
+Live validation update: the attestation v2 container image was successfully scheduled through the virtual node and the backing container group was created, but the resulting ACI container group came up as `sku=Standard` with `confidentialComputeProperties=null`. The app started and served HTTP, but runtime attestation failed because `/dev/sev-guest` was absent, which is the expected non-confidential behavior.
+
+Treat `virtual-node-confidential.yaml` as a scaffold for ongoing validation rather than a confirmed end-to-end confidential ACI-on-AKS sample until the virtual-node path is shown to preserve confidential ACI properties for the backing container group.
+
`virtual-node-confidential.yaml` is the starting point for that step. It includes the `microsoft.containerinstance.virtualnode.ccepolicy` annotation with an allow-all debug policy. Replace the policy value with the output of:
```bash