Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 29 additions & 9 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
.git
.github
.azure
.venv
__pycache__
*.pyc
*.pyo
*.pyd
*.log
*.db
.env
local.settings.json
bin
obj
artifacts
example_code
priv-folder
bin
obj
*.pyc
*.pyo
*.pyd
*.log
*.db

# These patterns are matched against the path relative to the build context, and a
# bare name only matches at the root, so the `**/` prefix is what makes them apply
# to front_end/ and api/ as well. Without it a local virtualenv or node_modules is
# copied into the image.
.venv
**/.venv
__pycache__
**/__pycache__
.pytest_cache
**/.pytest_cache

# Flask server-side session files hold live access tokens. They are runtime state
# and must never be baked into an image.
flask_session
**/flask_session

# Front end build artifacts. node_modules is restored inside the Node build stage,
# and static/dist is produced there, so neither should come from the host.
node_modules
**/node_modules
front_end/static/dist
36 changes: 36 additions & 0 deletions .github/workflows/front-end-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,42 @@ on:
- '.github/workflows/front-end-tests.yml'

jobs:
front-end-web:
name: Portal bundle (React)
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v4

- name: Set up Node version
uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: front_end/web/package-lock.json

- name: Install dependencies
working-directory: front_end/web
run: npm ci

- name: Type check
working-directory: front_end/web
run: npm run typecheck

- name: Run vitest
working-directory: front_end/web
run: npm test

# Catches a bundle that type checks but cannot actually be built, which is
# what the container image does during `az acr build`.
- name: Build the production bundle
working-directory: front_end/web
run: npm run build

front-end-test:
name: Portal BFF (Flask)
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -44,6 +79,7 @@ jobs:
run: pytest

api-test:
name: Broker API
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ priv-*
.vscode
flask_session/
.pytest_cache/

# Front end build artifacts
front_end/web/node_modules/
front_end/static/dist/
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The solution consists of the following components:

- **Azure Function for Scaling Tasks**: An Azure Function that runs on a schedule to manage scaling of Linux hosts based on the scaling rules. It updates VM network statuses, turns VMs on or off, and performs health checks on the Linux hosts.

- **Service Management Portal**: A front-end web application that allows administrators to manage VMs, scaling rules, and monitor the system. It provides functionalities such as adding/deleting VMs, checking out VMs, releasing/returning VMs, modifying VM statuses, and viewing logs.
- **Service Management Portal**: A front-end web application that allows administrators to manage VMs, scaling rules, and monitor the system. It provides functionalities such as adding/deleting VMs, checking out VMs, releasing/returning VMs, modifying VM statuses, and viewing logs. It is a React 18 and TypeScript single-page app built with Vite and Tailwind CSS, served by a Flask backend-for-frontend that holds the Entra ID token server-side and calls the Broker API on the administrator's behalf.

- **Azure Key Vault**: Stores sensitive information such as SSH keys and database passwords, accessed securely by the Broker API using managed identity.

Expand All @@ -50,7 +50,7 @@ The architecture ensures secure, efficient, and scalable management of Linux hos
- **Broker API**: RESTful API for brokering connections and managing VMs.
- **Broker Database**: Azure SQL Database for storing VM and scaling data.
- **Azure Function for Scaling Tasks**: Manages scaling of Linux hosts.
- **Service Management Portal**: Front-end application for administrators.
- **Service Management Portal**: React and TypeScript front-end application for administrators, served by a Flask backend-for-frontend.
- **Azure Key Vault**: Secure storage for SSH keys and passwords.
- **Managed Identities**: Used for secure authentication between components.
- **Security Groups**: Controls access permissions for managed identities.
Expand Down Expand Up @@ -259,7 +259,7 @@ For existing environments that need in-place rollout instead of new-environment

The deployment targets Azure commercial by default. Set `azureCloudName` to `AzureUSGovernment` or `AzureCustom` to deploy elsewhere; commercial and Government resolve their endpoints automatically, while custom and sovereign clouds require their own authority, Graph, STS, and App Service FQDNs. Air-gapped environments should also set `scriptSourceRoot` to a reachable mirror of this repository, because the Linux hosts download their agent scripts from it during bootstrap.

The Service Management Portal serves all of its front-end assets (Bootstrap, stylesheet, scripts and icons) from its own container under `front_end/static/`. It makes no requests to a public CDN, so the portal renders correctly in Government, sovereign and air-gapped environments where outbound internet access is blocked.
The Service Management Portal serves all of its front-end assets from its own container under `front_end/static/dist/`. The bundle is compiled during the container build, uses the system font stack, and draws its icons as inline SVG, so it makes no requests to a public CDN and renders correctly in Government, sovereign and air-gapped environments where outbound internet access is blocked. Note that building the portal image does require access to the npm registry, so a disconnected build host needs an internal npm mirror. See [front_end/README.md](front_end/README.md).

The deployment defaults the App Service plan to Premium v3 `P2mv3`, which provides the minimum supported baseline of 4 vCPUs and 32 GB memory for the frontend, API, and task apps.

Expand Down
9 changes: 9 additions & 0 deletions deploy/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Two details matter here:

- The supported path is `azd up` from the `deploy/` directory, not a separate manual mix of Bicep plus ad hoc scripts.
- Container images are built remotely with `az acr build`, so local Docker is not required.
- The `frontend` image is multi-stage and compiles the React portal in a Node stage, so the build host needs to reach the npm registry. See [Front end build requirements](#front-end-build-requirements).

For upgrade scenarios, keep one more distinction clear:

Expand Down Expand Up @@ -363,6 +364,14 @@ That means `postprovision` does all of the following:
- Adds AVD and Linux VM managed identities to the corresponding Entra groups.
- Registers Linux hosts into `dbo.VirtualMachines` through `dbo.RegisterLinuxHostVm`.

### Front End Build Requirements

The Service Management Portal is a React and TypeScript single-page app. [front_end/Dockerfile](../front_end/Dockerfile) is multi-stage: a `node:22-alpine` stage runs `npm ci` and `npm run build`, and only the compiled bundle is copied into the Python runtime image.

That means the machine performing the build, which is the ACR build agent when using `az acr build`, needs to pull the `node:22-alpine` base image and resolve packages from the npm registry. Nothing is fetched at runtime: the compiled bundle, the fonts, and the icons all ship inside the image, so the portal still renders in Government, sovereign and air-gapped environments.

If the build environment cannot reach `registry.npmjs.org`, point npm at an internal mirror before building, for example by adding an `.npmrc` with a `registry=` entry alongside [front_end/web/package.json](../front_end/web/package.json). `package-lock.json` is committed, so `npm ci` installs an exact, reviewable dependency set.

## Migration For Existing Deployments

Use the migration flow when you already have a deployed customer environment and want to roll forward the current application, SQL, and Linux-host release-agent changes without treating that as part of the normal `azd up` lifecycle.
Expand Down
22 changes: 22 additions & 0 deletions front_end/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# ---------------------------------------------------------------- web build
# The React portal is compiled here so the runtime image needs no Node toolchain
# and no network access. Everything it serves is bundled locally, which is what
# keeps the portal working in Azure Government, sovereign and air-gapped clouds.
FROM node:22-alpine AS web

WORKDIR /web

# Copied first so the dependency layer is only rebuilt when the manifests change.
COPY front_end/web/package.json front_end/web/package-lock.json ./
RUN npm ci

COPY front_end/web/ ./
# Vite writes to ../static/dist, which resolves to /static/dist from /web.
RUN npm run build

# -------------------------------------------------------------------- runtime
FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1
Expand All @@ -15,6 +32,11 @@ RUN pip install --no-cache-dir -r requirements.txt

COPY front_end/ ./

# The sources are not needed at runtime; only the compiled bundle is.
RUN rm -rf ./web

COPY --from=web /static/dist ./static/dist

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
Loading
Loading