Deploy-time VNet peering self-heal¶
Date: 2026-05-28
Type: infra / scripts
Files touched: scripts/dev/postprovision.sh, scripts/dev/grant-runtime-rbac.sh, scripts/dev/peer-cluster-network.sh, deploy.sh
Motivation¶
External-cluster join flow (the ELB_CLUSTER_RG_NAME path, where the dashboard
peers with an AKS cluster created outside azd up) left two things manual:
- The dashboard MI never received
Network Contributoron the AKS-auto VNet inMC_<cluster_rg>_<cluster>_<region>. Without it, the platform-side peer creation passes but the cluster-side mirror call fails withLinkedAuthorizationFailed/Microsoft.Network/virtualNetworks/peer/action … not authorized. - Even with RBAC in place, no automation triggered the actual peering. The
first time the SPA tried to reach the cluster's elb-openapi endpoint the
user got
openapi_upstream_unreachable: missing VNet peering …and had to curlpeer-cluster-network.shby hand.
This produced a recurring "first click after deploy fails" experience that the
existing self-heal pattern (grant-runtime-rbac.sh already called from
postprovision) was meant to prevent.
User-facing change¶
azd up/azd provisionnow do both of these end-to-end:- Grant
Network Contributoron the AKS-auto VNet in addition toContributor+User Access Administratoron the cluster RG. - Create / verify bidirectional VNet peering between the dashboard platform VNet and the cluster's aks-auto VNet.
- New opt-out:
ELB_SKIP_AUTO_PEER=trueskips only the peering call (the RBAC grant always runs because it's the prerequisite). Use when an external operator manages peering out of band, or on firstazd upbefore any AKS cluster exists. peer-cluster-network.shnow resolves the target AKS cluster via the samemanagedBy=elb-dashboard+azd-env-nametag filter thatgrant-runtime-rbac.shuses, so subscriptions with multiple AKS clusters (dev / test / prod side-by-side) no longer require explicit--cluster-name/--cluster-rgflags for the common case.
Diff summary¶
scripts/dev/grant-runtime-rbac.sh¶
- Header docstring updated to mention Network Contributor on the aks-auto VNet and the peering rationale.
ASSIGNMENTSarray construction moved before the plan-summary + confirmation prompt so the operator sees all three roles up front.- New dynamic-resolution block (only outside bootstrap mode): for every AKS
cluster in
CLUSTER_RG, look upnodeResourceGroup, find the aks-auto VNet viaaz network vnet list, and append aNetwork Contributorentry scoped to that VNet's ARM id. BYO-VNet clusters (no VNet in the node RG) are skipped with an[info]line — no failure, no false-positive grant.
scripts/dev/peer-cluster-network.sh¶
- JMESPath flatten fix (committed earlier in session): the original
containers[?name=='api'].env[?name=='X']always returned[]; replaced withcontainers[?name=='api'].env[] | [?name=='X']for bothAPI_CLIENT_IDandPLATFORM_PRIVATE_ENDPOINT_SUBNET_IDqueries. - AKS cluster auto-detect upgraded to a 4-step precedence:
--cluster-name+--cluster-rgflags (explicit operator intent).ELB_CLUSTER_RG_NAME(env or azd env) — pick the single cluster in that RG.- Tag filter:
managedBy=elb-dashboard, narrowed byazd-env-namewhen present. Mirrorsgrant-runtime-rbac.sh. - Legacy fallback: subscription has exactly one AKS cluster.
- Multi-match still refuses with rc=3 + the matching cluster list, suggesting
--cluster-name/--cluster-rgorELB_CLUSTER_RG_NAME.
scripts/dev/postprovision.sh¶
- New Section 5 (after the existing Section 4 RBAC self-heal): if
ELB_SKIP_AUTO_PEERis nottrue, invokescripts/dev/peer-cluster-network.sh --yesagainst the freshly-deployed Container App. Output is indented under. Exit-code handling: 0→ "✓ VNet peering OK".3→ "ⓘ skipped (no AKS cluster yet, or ambiguous)" with hint to pass--cluster-name+--cluster-rg.2→ "⚠ partial — re-run", typically transient RBAC-propagation race.- other → manual recovery hint.
- Best-effort: failure never breaks the deploy.
deploy.sh¶
- New
ELB_SKIP_AUTO_PEERenv var documented in the usage block right afterELB_CLUSTER_RG_REGION.
API / IaC impact¶
- No Python, Bicep, or frontend code changed.
- No new dependencies.
- Backward compatible:
grant-runtime-rbac.shstill works in bootstrap mode (cluster RG missing → skip the new resolver block). Dry-run output unchanged for that case.peer-cluster-network.shretains its existing CLI: the recovery commands rendered byapi/tasks/azure/peering.py(which always pass explicit--cluster-rg --cluster-name --subscription) take the same path as before.
Validation evidence¶
Syntax (bash -n)¶
$ bash -n scripts/dev/grant-runtime-rbac.sh && echo OK
OK
$ bash -n scripts/dev/postprovision.sh && echo OK
OK
$ bash -n scripts/dev/peer-cluster-network.sh && echo OK
OK
$ bash -n deploy.sh && echo OK
OK
grant-runtime-rbac.sh --dry-run¶
[…] Container App: ca-elb-dashboard (rg-elb-dashboard)
[…] Dashboard MI: e51aaab3-eb17-4935-a7eb-446b53a5c445
[…] AKS cluster RG: rg-elb-cluster
[…] Plan (3 role assignment(s)):
[…] - Contributor @ /subscriptions/…/resourceGroups/rg-elb-cluster
[…] - User Access Administrator @ /subscriptions/…/resourceGroups/rg-elb-cluster
[…] - Network Contributor @ /subscriptions/…/resourceGroups/mc_rg-elb-cluster_elb-cluster-01_koreacentral/providers/Microsoft.Network/virtualNetworks/aks-vnet-23268255
(dry-run — no role assignments will be created)
[skip] Contributor already assigned at …/rg-elb-cluster
[skip] User Access Administrator already assigned at …/rg-elb-cluster
[dry ] would assign Network Contributor at …/Microsoft.Network/virtualNetworks/aks-vnet-23268255
[…] Summary: created=0 skipped=2 failed=0
openapi_upstream_unreachable today.
peer-cluster-network.sh --dry-run --yes (no explicit cluster flags)¶
Subscription had 5 AKS clusters; tag filter resolved correctly to the elb-dashboard cluster without forcing the operator to specify it:
[…] Container App: ca-elb-dashboard (rg-elb-dashboard)
[…] AKS cluster: elb-cluster-01 (rg=rg-elb-cluster)
[…] Endpoint: POST https://ca-elb-dashboard.…/api/aks/peer-with-platform
[dry ] would POST … with cluster_name=elb-cluster-01 resource_group=rg-elb-cluster
Self-review notes¶
- Consumer search confirmed no Python or test caller invokes the scripts
at runtime — they only embed the script name in recovery-command strings.
All such callers (
api/tasks/azure/peering.py,api/tasks/azure/rbac.py,api/routes/aks/openapi.py,api/tests/test_azure_peering.py,api/tests/test_azure_tasks.py) keep passing explicit--cluster-rg --cluster-name --subscription, so the new tag-based auto-detect path is purely additive. - No
.py/.ts/.tsx/.bicepfiles modified — pytest, ruff, andnpm run buildare not required for this change set. git status --shortshows only the four intended files dirty among the scripts touched here (plus pre-existing unrelated WIP).
Manual rollback¶
ELB_SKIP_AUTO_PEER=true ./deploy.shskips the new peering step.- To remove the Network Contributor grant:
az role assignment delete --assignee <MI principalId> --role "Network Contributor" --scope <aks-vnet ARM id>. - To remove the peering:
az network vnet peering delete -g <platform_rg> --vnet-name <platform_vnet> -n <peer_name> - the mirror on the aks-auto VNet side.