Browser-only control plane for ElasticBLAST on Azure.
A researcher signs in through the browser, opens the embedded Browser Terminal sidecar when command-line work is needed, and monitors AKS / Storage / ACR / Job state from a glassmorphic dashboard. The user never opens a local terminal during steady state; local commands are only for developers or operators bringing up the control plane itself.
Agent navigation map: AGENTS.md · Live docs: https://dotnetpower.github.io/elb-dashboard/ · Source stamp:
scripts/dev/version-stamp.sh(refresh README badge withscripts/dev/version-stamp.sh --update-readme)
| You are a… | Start here |
|---|---|
| Researcher running BLAST | https://dotnetpower.github.io/elb-dashboard/user-guide/ — no checkout required |
| Operator deploying the control plane | Quick start: deploy to Azure ↓ |
| Contributor changing the code | Get started guide → Contributing ↓ |
| AI agent | AGENTS.md (route map + tripwires) → .github/copilot-instructions.md (charter) |
- Dashboard preview
- Architecture at a glance
- Layout
- Quick start: deploy to Azure
- Prerequisites
- Local development
- Driving a deployed environment from your laptop
- Roadmap
- Authentication (production path)
- Contributing
- License
A single glance shows every moving part of an ElasticBLAST run on Azure:
- Azure Kubernetes Service Cluster — node-pool CPU/memory live, cluster
state, kubelet object id, and which BLAST databases are pre-warmed on each
cluster (
16S_ribosomal_RNA 3/3,core_nt 0/3). Start/stop/delete actions are inline. - Azure Container Registry — login server, SKU, and the four pinned
ElasticBLAST images (
ncbi/elb 1.4.0,elasticblast-job-submit 4.1.0,elasticblast-query-split 0.1.4,elb-openapi 4.14) with build status per image. A one-click Build kicks offaz acr buildvia a Celery task on the worker sidecar. Source of truth: api/services/image_tags.py. - Storage Account — region, SKU, HNS state, and the read-only
publicNetworkAccessindicator (always Disabled in steady state — see docs/container-apps-migration.md §Storage Network Isolation). The container row shows blob counts and last-update times forblast-db,queries, andresults; the BLAST Databases chip row reflects what is ready for immediate use. - Browser Terminal —
terminalsidecar process state, lastaz loginheartbeat, and an Open button that launches the embedded shell (xterm.js over a same-origin WebSocket → loopbackttyd). No SSH, no password reveal. - BLAST Jobs — submission history with status, elapsed time, and drill-down to the Celery task's full event history from Table Storage. The card is empty in this screenshot because no jobs were submitted yet.
Subscription name and the kubelet object id are masked in this screenshot. The dashboard renders the real values when you sign in.
One Azure Container App revision (ca-elb-dashboard) bundles six sidecars.
The public ingress only targets the api sidecar on port 8080; everything else
talks loopback.
flowchart LR
user([Browser])
subgraph CA["Azure Container App · ca-elb-dashboard"]
direction LR
api["api<br/>FastAPI :8080"]
fe["frontend<br/>nginx :8081"]
term["terminal<br/>ttyd 127.0.0.1:7681<br/>elastic-blast CLI"]
worker["worker<br/>Celery"]
beat["beat<br/>Celery scheduler"]
redis["redis<br/>broker · ephemeral"]
end
stg[("Azure Storage<br/>tables + blobs")]
arm{{"Azure ARM<br/>AKS · ACR · KV"}}
user -- HTTPS --> api
api -- reverse proxy --> fe
api -- WebSocket --> term
api -- enqueue --> redis
beat -- schedule --> redis
redis -- dispatch --> worker
worker -- progress --> stg
api -- read/write --> stg
worker -- ARM via MI --> arm
term -- elastic-blast --> arm
beat -. reconcile from jobstate .- stg
term -. azcopy stage .- stg
State: durable rows in Azure Storage Tables, append blobs for command
history. Sidecars are ephemeral — on revision restart, beat rebuilds the
in-flight queue from the jobstate table and the terminal re-authenticates
with the Managed Identity (user files stage to workload Storage via azcopy,
not to a local mount). No managed database, no Service Bus, no Static Web
App, no Remote Terminal VM, no Azure Files shares.
api/ Backend — FastAPI for the api sidecar + Celery worker/beat (also shared Azure SDK service wrappers and HTTP boundary helpers)
web/ React + Vite + TypeScript SPA + Dockerfile + nginx.conf for the frontend sidecar
terminal/ Dockerfile + entrypoint for the terminal sidecar (ttyd + elastic-blast toolchain + exec_server)
infra/ Bicep IaC (network, identity, ACR, storage, Key Vault, Container Apps Env, Container App)
scripts/ Dev helpers + `postprovision.sh` (builds images, swaps the Container App template)
docs/ Architecture notes, per-feature change log, and the published docs site source
This repo is hardened so a fresh git clone can deploy the full
control-plane bundle (one Container App with six sidecars, a private VNet, a
locked-down Storage account, an ACR, a Key Vault, and Log Analytics) with a
single azd up. Cost is roughly USD 130/month in koreacentral for the
default sizing (see docs/container-apps-migration.md §Cost Estimate).
For first-time setup, especially on Windows, use the guided walkthrough first: docs/get-started.md.
# macOS / Linux
curl -fsSL https://aka.ms/install-azd.sh | bash
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash # or `brew install azure-cli`
sudo apt install -y jq curl # `brew install jq curl`Verify:
./scripts/dev/preflight-check.shaz login
az account set --subscription "<your-subscription>"The deployment creates or reuses the App Registration automatically during azd up. If your tenant blocks App Registration creation, ask an Entra administrator to create it once and set API_CLIENT_ID in the azd environment.
azd env new elb-dashboard
azd env set AZURE_LOCATION koreacentral
# Optional: same-origin only (recommended once the SPA is served by the
# frontend sidecar). Leave empty for that.
# azd env set ALLOWED_ORIGINS "https://my-other-spa.example.com"
# Optional: leave private networking off for the very first deploy so the
# postprovision hook can push images and seed secrets, then flip on a
# second `azd provision`.
azd env set LOCKDOWN_PRIVATE_NETWORKING falseFor the shortest fresh-clone path, run the bootstrap wrapper. It checks az account show, starts az login if needed, prepares the elb-dashboard azd environment so the resource group is rg-elb-dashboard, runs azd up, and opens the deployed Container App URL:
./deploy.shIf you prefer the raw azd command after preparing/selecting the environment, run:
azd upazd up runs:
The command prints an azd up progress map before long-running work starts,
then marks the active step as [n/8] while it runs.
- preprovision — registers deployment Azure resource providers
(
Microsoft.App,Microsoft.Authorization,Microsoft.ContainerRegistry,Microsoft.Storage, etc.) and starts first-run workflow provider registration for Compute, ContainerService, and Quota. Ifrg-elb-dashboardalready contains resources, the hook asks whether to delete it and continue, keep it and deploy to the next numbered group such asrg-elb-dashboard-01, or abort. Non-interactive runs can setELB_EXISTING_RG_ACTION=delete|number|abort. - provision — runs infra/main.bicep which creates the platform RG, VNet (3 subnets), Log Analytics, optional App Insights, the shared user-assigned managed identity, the Premium ACR, the Standard_LRS Storage account (with state tables / blob containers), the Key Vault, the Container Apps Environment, and the Container App seeded with a hello-world bootstrap image.
- postprovision — runs scripts/dev/postprovision.sh
which builds the three images (
elb-api,elb-frontend,elb-terminal) viaaz acr build(no local Docker needed) and runsaz deployment group createto swap the Container App template to the six-sidecar layout.
When it finishes you get an HTTPS URL like
https://ca-elb-dashboard.<subdomain>.koreacentral.azurecontainerapps.io.
Verify the deployment in this order:
APP_URL=$(az containerapp show -g rg-elb-dashboard -n ca-elb-dashboard \
--query properties.configuration.ingress.fqdn -o tsv)
curl -s "https://$APP_URL/api/health" | jq . # api sidecar
curl -sI "https://$APP_URL/" | head -1 # frontend sidecar serves the SPAThen open https://$APP_URL/ in a browser, sign in with the MSAL popup, and
you should land on the Dashboard. The next things to click:
- Cluster card → Start — creates the first AKS node pool (one-time).
- Browser Terminal → Open — should drop you into the
terminalsidecar withelastic-blast --versionavailable. - BLAST → Submit — walks you through a query against
16S_ribosomal_RNA.
If any card renders network_blocked or access_denied from your laptop, see
Driving a deployed environment from your laptop below.
The first deploy keeps Storage / Key Vault / ACR public so az acr build
and Key Vault seeding can run from the operator's machine. The second
deploy flips publicNetworkAccess to Disabled on all three and adds
private endpoints:
azd env set LOCKDOWN_PRIVATE_NETWORKING true
azd provisionAfter this point the only client that can reach platform Storage / Key Vault / ACR is the Container App, over private endpoints inside the platform VNet (see docs/container-apps-migration.md §Storage Network Isolation).
azd down --purge --forceRemoves the platform RG and purges Key Vault soft-deletes.
| Tool | Minimum | Notes |
|---|---|---|
| Azure CLI | 2.81+ | Run az login first |
| azd | 1.10+ | curl -fsSL https://aka.ms/install-azd.sh | bash |
| uv | 0.9+ | curl -LsSf https://astral.sh/uv/install.sh | sh — drives Python tooling |
| Python | 3.12 | Provided by uv sync (does not need to be installed system-wide) |
| Node.js | 20 LTS | For the SPA |
| Docker | 20.x+ | Optional — only needed for scripts/dev/docker-compose.local.yml |
| jq, curl | any | sudo apt install jq curl |
Deeper architecture reference: docs/container-apps-migration.md.
Local backend bring-up:
uv sync --all-groups # creates .venv on Python 3.12 + installs runtime + dev tools
uv run pytest -q api/tests # ~980 tests
scripts/dev/local-run.sh apiVS Code dev tasks and direct terminal runs through scripts/dev/local-run.sh
mirror local pipeline logs into .logs/local/latest/ inside this project. The
newest 3 sessions are retained, each log chunk is capped at 1 MiB, and each
service keeps a bounded 16-chunk ring per session. Start with
.logs/local/latest/api.log, then check worker.log, beat.log, web.log,
and smoke.log when diagnosing warnings, errors, or pipeline health.
Docker Compose runs should go through scripts/dev/local-run.sh compose-full
or compose-local; detached compose runs also create
compose-full-containers.log / compose-local-containers.log with container
stdout/stderr and replay only the newest 200 lines by default.
If a teammate has already deployed the dashboard with azd up and you only
want to run the SPA / backend locally against that environment, do not
hand-edit web/.env.local with a clientId. Run azd env refresh -e <env-name>
to bind this clone to the existing azd environment, then start the web tier
with scripts/dev/local-run.sh web — it auto-exports VITE_AZURE_CLIENT_ID
from the deployed API_CLIENT_ID. Full walkthrough:
docs/joining-existing-deployment.md.
When something is already broken, start with
docs/troubleshooting.md.
One-time RBAC + network setup. The local api uses DefaultAzureCredential →
your az login identity, which starts with zero RBAC on the workload
Storage / ACR. Without the steps below the dashboard will render
network_blocked / access_denied and DB downloads will fail with HTTP 403.
# 1. one-shot: grant your az user the minimum roles on the deployed environment.
# Defaults match docs/auth.md (storage=elbstg01 in rg-elb-01, acr=elbacr01 in rg-elbacr-01).
# Override with --storage / --storage-rg / --acr / --acr-rg if your deployment differs.
scripts/dev/grant-local-rbac.sh # add --dry-run to preview
# wait 1-5 min for RBAC propagation, then:
# 2. start the api with the local-debug Storage auto-open helper enabled —
# this opens publicNetworkAccess for your caller IP only when needed.
LOCAL_DEBUG_AUTO_OPEN_STORAGE=true \
AUTH_DEV_BYPASS=true \
scripts/dev/local-run.sh api
# 3. when you're done debugging, close the network surface again:
scripts/dev/storage-public-access.sh offBoth helpers are idempotent; both refuse to act inside a Container App
(CONTAINER_APP_NAME env present). See
scripts/dev/README.md and
.github/copilot-instructions.md §9.
Tracked work that is not yet shipped:
- Streaming upload/download proxy backing for the SPA
BlastResultsdownload/export buttons (charter §9 keeps StoragepublicNetworkAccessoff, so these must stream through theapisidecar — no SAS tokens). - CI pipeline for
azd upagainst an ephemeral subscription. - Smaller polish items live in docs/releases/unreleased.md.
In production (AUTH_DEV_BYPASS=false):
- SPA acquires an MSAL access token for
api://<client-id>/user_impersonation. - The api sidecar validates the JWT against the tenant's OIDC discovery + JWKS.
- Backend uses the shared user-assigned Managed Identity
id-elb-dashboard-*(mounted on the Container App, visible to all sidecars) for downstream ARM and data-plane calls. The browser token proves who called; it is not exchanged for Azure resource tokens. - The
terminalsidecar inherits the same MI —az login --identityworks out of the box. Device-code login is only needed when a user intentionally wants a personal Azure CLI session.
AUTH_DEV_BYPASS=true short-circuits step 2 and lets the API call Azure
with whatever credential DefaultAzureCredential finds (typically your
local az login). Never enable this in production.
PRs welcome. Before opening one:
- Read the rules. .github/copilot-instructions.md
is the project charter (Python 3.12 +
uv, nopip install, norequirements.txt, no Azure Functions, StoragepublicNetworkAccessstaysDisabled, no SAS tokens to the browser, no Azure Run Command). AGENTS.md is the route + tripwire map. - Validate locally. Every behaviour-changing PR must pass:
uv run pytest -q api/tests # ~980 tests uv run ruff check api # lint cd web && npm run build # SPA compiles cleanly
- Write a change note under
docs/features_change/YYYY-MM/YYYY-MM-DD-<short-name>.mddescribing motivation, user-facing change, API/IaC diff, and validation evidence. - Conventional Commits.
feat:,fix:,chore:,docs:, … — English only in source, commits, docs, and UI strings (Korean is fine in PR conversation). - No new dependency without justification in the PR description.
- Release version bumps go through scripts/dev/bump-version.sh
— do not hand-edit
versioninweb/package.jsonorpyproject.toml. Frontend build numbers are computed at build time from commits since the latest release tag. Full policy: docs/copilot/version-management.md.
MIT © elb-dashboard maintainers. ElasticBLAST itself is developed at NCBI under the U.S. Government public-domain notice — see the upstream repo and the NCBI ElasticBLAST documentation for the runtime license.
