Skip to content

Refresh deployment for the React portal and host settings - #32

Merged
Paul Lizer (paullizer) merged 5 commits into
mainfrom
paullizer-broker-deployment-refresh
Sep 23, 2026
Merged

Paul Lizer (paullizer) merged 5 commits into
mainfrom
paullizer-broker-deployment-refresh

Conversation

@paullizer

@paullizer Paul Lizer (paullizer) commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes the gaps found while redeploying a complete environment with the new React portal, host settings, and hardening work, plus the problems that showed up while validating that deployment end to end.

Infrastructure (Bicep)

  • Private DNS for Linux hosts: a linuxbroker.internal zone with VNet auto-registration (only when domainName is empty). The API and task apps now use VNet integration on the app subnet, so the API can reach hosts at <host>.linuxbroker.internal over SSH.
  • Gen2 RHEL images: RHEL 7/8/9 now use 7lvm-gen2, 8-lvm-gen2, and 9-lvm-gen2.
  • NFS profile share (optional, on by default): a Premium FileStorage account with an NFS home share, a private endpoint, and a privatelink.file zone. Its path is wired into NFS_SHARE automatically unless nfsShare is set.
  • API VM power control: the API identity gets Desktop Virtualization Power On Off Contributor on the host VM resource group.
  • AVD access:
    • Publishes a Linux Desktop RemoteApp that runs Connect-LinuxBroker.ps1.
    • Sets the Entra SSO RDP properties.
    • Gives an optional users group Desktop Virtualization User on the RemoteApp group and Virtual Machine User Login on each session host.
    • Passes the API client ID and script root to the host extension.

Scripts

  • Initialize-DeploymentEnvironment.ps1:
    • Creates the AVD users group, adds the deployer, and passes it to Bicep.
    • Grants admin consent for the frontend app.
    • Turns on Entra RDP authentication for Windows Cloud Login where it can.
    • Adds defaults for deployNfsShare, nfsShareQuotaGiB, and avdUsersGroupId.
  • Assign-VmApiRoles.ps1: retries and re-checks each host identity's group membership, and fails instead of silently continuing.
  • api/Dockerfile: installs openssh-client.
  • Configure-RHEL9-Host.sh: fixes the xpra repository URL (the old one returns 404 and failed the bootstrap under set -e).
  • Configure-AVD-Host.ps1 and Connect-LinuxBroker.ps1:
    • Configure the API client ID and download from the script root.
    • Store credentials for TERMSRV/ targets.
    • Fix the event log entry types.

Fixes from validating the deployment

  • Every checkout failed with a 500 (Load key "/tmp/private_key.pem": error in libcrypto). The deployment hooks trim the private key's trailing newline on the way into Key Vault, and OpenSSH will not load a key without one. The API now normalizes the key before writing it (API 0.160, with a regression test).
  • Released hosts were never returned to the pool. Post-provision started the task image before assigning its ScheduledTask role. The function app's first token had no role, and the managed identity service caches it for up to 24 hours, so a restart does not help. The role is now assigned before the images are built. Assign-ServicePrincipalApiRole.ps1 also retries and throws on failure; it used to print success after a failed call.
  • NFS home directories never roamed, and session files were deleted at return. create-user.sh bind-mounts the user's NFS home at checkout. The API's own SSH logins during checkout wake the logind watcher, and check_unmount_user_homes then unmounted any home whose user had not signed in yet, which was always true at that point. Every session ran on the local disk, and userdel -r deleted it when the host was returned. The Ubuntu agent never unmounts, so there userdel -r would have deleted the profile from the share instead.
    • The RHEL release agent now leaves a home mounted while the host holds that user's lease.
    • create-user.sh writes the lease before it mounts the home, so the agent never sees a mounted home without one.
    • manage-lease.sh clear and clear-any unmount the home before they remove the lease. If the user is still signed in, they remove the lease but leave the account and the mount alone.
    • The API clears the lease before it runs userdel -r on both return paths (the path without a lease used to delete first), and it refuses to run userdel -r while the home is still mounted (API 0.161).
    • A checkout that fails after create-user.sh has written the lease now cleans up the host before the VM goes back to the pool (API 0.162).
  • xpra on RHEL 9: the upstream 6.5 proxy exits during startup (its unit binds QUIC, and aioquic is not packaged for EL9). systemctl enable --now still returns 0, so the old warning never fired and the host was left degraded. The bootstrap now checks that the unit stays up and disables it if not. xrdp is unaffected.
  • Documented these, plus SkuNotAvailable and a stale Entra device that blocks the session host join.

Docs

Updated DEPLOYMENT.md, the parameter example, api/README.md, and README.md. This includes how to point scriptSourceRoot at a branch or fork commit, the new post-provision order, and troubleshooting for each issue above, including home directories that are not on the share.

Validation

Tested with a fresh deployment in East US 2: RHEL 9 with GNOME, the NFS share, two Linux hosts, and one AVD session host.

  • az bicep build and az bicep lint are clean. The PowerShell parser reports no errors on the changed scripts, bash -n passes on the RHEL 9 script, and the API tests pass (56). The new lease and checkout tests fail against the previous code.
  • Checkout from the AVD host (with its managed identity) took about 6 seconds on each Linux host. It created the user with a centrally allocated UID, wrote the lease marker, and returned a host reachable on 3389 by its private DNS name.
  • Desktop session: a GNOME session started through xrdp-sesman for a broker-created user.
  • Release: the host release agent detected the disconnect, released the VM with the matching lease, and started the grace period from host settings version 1.
  • Return: the function app returned released VMs and deleted their users, which left no accounts, leases, or mounts behind.
  • Roaming profile: a user ran a GNOME session on lnxhost-02 with the home on the share, signed out, and the host was returned. The profile stayed on the share (84 files). The same user then checked out lnxhost-01, which mounted that home with the file written on lnxhost-02, ran another GNOME session there, and was returned cleanly again.
  • manage-lease.sh: tested on a host against a bind-mounted home with a matching, mismatched, and missing lease, a signed-in user, clear-any, and an invalid username. The reordered create-user.sh was run on both hosts against the real share, followed by manage-lease.sh clear and userdel -r; the files on the share survived.
  • AVD: the session host is Entra joined and Available. The Linux Desktop RemoteApp and the users group assignments are in place.
  • Portal: the React portal loads and redirects to Microsoft Entra with the API scope.

Known issues (existing behavior, not changed here)

  • Released VMs can take up to an hour to return. The hourly connectivity check calls UpdateVmAttributes, which resets LastUpdateDate and restarts the 30-minute return timer.
  • Scaling never powers VMs on or off. TriggerScalingLogic returns PoweredOn/PoweredOff, but the API compares against PowerOn/PowerOff. The database state changes, but no Azure power operation runs.
  • Hosts report degraded on AMD sizes. mcelog.service fails there; this is cosmetic.

Notes

  • If you deploy from this branch before it merges, set scriptSourceRoot to this branch's commit. The hosts otherwise download the scripts from main, and the main scripts don't accept the new parameters.
  • The validation hosts were built from 81becfe. The xpra change, create-user.sh, manage-lease.sh, and the RHEL release-session.sh were updated on them by hand.

Close the gaps found while redeploying a full environment:

- Networking: add a linuxbroker.internal private DNS zone with VNet
  registration and integrate the API and task apps with the app subnet,
  so the API can reach Linux hosts by name over SSH.
- API image: install openssh-client for host operations.
- Linux hosts: use Gen2 RHEL 7/8/9 LVM images and fix the RHEL 9 xpra
  repository URL so the bootstrap no longer fails under set -e.
- Profiles: optionally deploy a Premium Azure Files NFS share behind a
  private endpoint and wire its path into NFS_SHARE.
- RBAC: grant the API identity Desktop Virtualization Power On Off
  Contributor on the host VM resource group.
- AVD: publish a Linux Desktop RemoteApp, enable Entra SSO RDP
  properties, grant an optional users group access and VM User Login,
  and pass the API client ID and script root to the host extension.
- Preprovision: create and wire the AVD users group, consent the
  frontend app, and enable Entra RDP authentication when possible.
- Post-provision: retry and verify host identity group membership.
- Docs: update DEPLOYMENT.md, the parameter example and READMEs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- API: restore the private key's trailing newline before writing it for
  SSH. The deployment hooks trim it on the way into Key Vault, and OpenSSH
  refuses the key ("error in libcrypto"), so every checkout failed.
- Post-provision: assign the ScheduledTask role before the images start.
  The function app requested its API token first, and the managed identity
  service caches that role-less token for up to 24 hours, so released
  hosts were never returned. The role assignment now retries and fails
  loudly instead of reporting success after a failed call.
- RHEL 9 bootstrap: disable xpra when its proxy exits during startup, as
  the upstream 6.5 packages do on RHEL 9, instead of leaving a failed unit.
- Document these, SkuNotAvailable, and a stale Entra device that blocks
  the session host join.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The release agent unmounted a user's home whenever the user was not signed
in. The broker mounts the home at checkout, before the user connects, and its
own SSH login wakes the logind watcher, so the home was unmounted about two
seconds after every checkout on the test deployment. Sessions then ran on the
local disk, and userdel -r deleted that data at return. Profiles never roamed.

- release-session.sh (RHEL) keeps the home mounted while the host holds the
  user's lease.
- manage-lease.sh unmounts the home before it clears the lease, so userdel -r
  only removes the empty local mount point. If the user is still signed in,
  it clears the lease but leaves the home and account alone.
- delete_remote_user runs manage-lease.sh first on both paths and refuses to
  run userdel -r while the home is still mounted. API 0.161.
- DEPLOYMENT.md explains the symptom and the fix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
create-user.sh can fail after it writes the lease marker, and hosts now keep
the NFS home mounted for as long as the lease exists. The checkout failure path
only released the database assignment, so the account, lease, and mount stayed
behind. It now runs delete_remote_user before it returns the VM to the pool.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The RHEL release agent now keeps a mounted home only while its lease exists.
create-user.sh bind-mounted the home before it wrote the lease, which left a
short window where the agent could unmount it. Also note in manage-lease.sh
that only the RHEL agent unmounts homes after a user signs out.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) marked this pull request as ready for review September 23, 2026 16:55
@paullizer
Paul Lizer (paullizer) merged commit e86011e into main Sep 23, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant