Skip to content

Azure deployment — Simple shape

Companion to the Bicep in /azure. One-click install: see the README's Deploy to Azure button. Target: customers whose networking is owned by a central CCoE. No VNet, no private endpoints, no public IP that you provision.

What you get

                                    Internet
                                       ▼  https://<app>.azurewebsites.net
                                       │  (Microsoft-managed TLS cert)
                    ┌──────────────────────────────────────┐
                    │  App Service Plan (Linux)            │
                    │  ┌──────────────────────────────┐    │
                    │  │  App Service                 │    │
                    │  │  Pulls ghcr.io/fortigi/      │    │
                    │  │   identity-atlas:latest      │    │
                    │  │  Mounts /data/uploads        │    │
                    │  │  KV refs for master key +    │    │
                    │  │   DB password                │    │
                    │  └──────────────────────────────┘    │
                    └──────────────────────────────────────┘
            ┌──────────────────────────┼─────────────────────────────┐
            ▼                          ▼                             ▼
   ┌──────────────────┐     ┌────────────────────┐        ┌──────────────────┐
   │ Postgres Flex    │     │ Azure Files share  │        │ Key Vault         │
   │ Public endpoint, │     │ (Storage Account)  │        │ Public endpoint,  │
   │ "Allow Azure     │     │ /data/uploads      │        │ RBAC-only.        │
   │  services"       │     │ shared with worker │        │ master key +      │
   │ firewall rule    │     └────────────────────┘        │ DB password.      │
   └──────────────────┘                                   └──────────────────┘

                ─── Container Apps Environment (Consumption, no VNet) ───
                    ┌──────────────────────────────────────┐
                    │  Container App: worker (always-on)   │
                    │  Pulls ghcr.io/fortigi/              │
                    │   identity-atlas-worker:latest       │
                    │  Mounts the same /data/uploads share │
                    │  Polls App Service /api for queued   │
                    │   crawler jobs every 60s             │
                    └──────────────────────────────────────┘

Inventory: 8 resource families (App Service Plan + App Service, Postgres, Storage, Key Vault, Log Analytics, ACA Environment, ACA App, managed identities). No VNet, no private endpoints, no NSG.

Sizing — pick at deploy time

The Deploy-to-Azure form shows a sizeProfile dropdown. Pick what matches your tenant.

Profile App Service Postgres Worker ~€/mo Use case
xs B1 (1 vCPU / 1.75 GB) B1ms Burstable (1 vCPU / 2 GB) 0.25 vCPU / 0.5 GB 45 Demo / proof-of-concept / non-production
s ✅ default B2 (2 vCPU / 3.5 GB) B2s Burstable (2 vCPU / 4 GB) 0.25 vCPU / 0.5 GB 79 Small production — single team of analysts, < 10k principals
m S1 (+ staging slot) B2s Burstable 0.25 vCPU / 0.5 GB 113 Mid-sized, 10-25k principals, blue/green deploys
l P1v3 (2 vCPU / 8 GB) D2ds_v5 GeneralPurpose (2 vCPU / 8 GB) 0.5 vCPU / 1 GB 244 Large tenant, 25-50k principals, sustained throughput
xl P2v3 (4 vCPU / 16 GB) D4ds_v5 GeneralPurpose (4 vCPU / 16 GB) 0.5 vCPU / 1 GB 469 Enterprise — 50k+ principals, multiple concurrent analyst teams

(All West Europe, ex VAT, single replica, no HA, Linux. Costs assume BYO Log Analytics; add ~€3-5/mo if a new workspace is created.)

Scaling later = re-click the button (or rerun deploy.ps1) with a different sizeProfile. Azure resizes the App Service Plan + Postgres in place — no data loss, ~2-3 min of Postgres downtime while the SKU swap happens.

One-way ratchet: l and xl use Postgres GeneralPurpose. You can't scale a Postgres Flex server from GeneralPurpose back to Burstable — only within the same tier. Pick l/xl only when you actually need the GP compute.

"Fast and snappy" design choices

  • Always On on the App Service Plan (B1+) — no cold start on first request.
  • Worker as ACA App, always-onSync now picks up jobs within 60s (matches the docker-compose worker behaviour).
  • App Service health probe on /api/health — drops sick replicas before users hit them.
  • Postgres autogrow on, HA off — no failover blip during a sync.
  • Materialised matrix view refreshed at end of every crawl — matrix renders from indexed pre-computed rows.

xs has caveats under concurrent load (3+ analysts on the matrix); it's labelled "non-production." Default s is where snappy starts being free of caveats.

BYO Log Analytics

If your CCoE owns a central Log Analytics workspace and you want logs forwarded there instead of creating a new one, fill in one of these two parameter pairs:

Option 1 — workspace ID (preferred, 1 field):

existingLogAnalyticsWorkspaceId: /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.OperationalInsights/workspaces/<name>
The template uses an existing resource lookup to derive the workspace customer ID + shared key. The deployer needs Log Analytics Reader on the workspace.

Option 2 — customer ID + key (fallback when you can't read the workspace, 2 fields):

existingLogAnalyticsCustomerId: <GUID>     (shown in the LA "Overview" blade as "Workspace ID")
existingLogAnalyticsSharedKey:  <primary or secondary key>
Use this when the deployer can't be granted Reader. The template plumbs the values through directly; no read access needed.

Leave all three empty → template creates a fresh workspace in your resource group (~€3-5/mo).

The deployment's outputs include logAnalyticsCreated: true|false so you can tell which path was taken.

How it deploys (timing)

  1. Storage, Log Analytics (or lookup), Managed Identities, Key Vault — all parallel, < 1 min.
  2. Bootstrap deployment script — runs as the deployScript managed identity. Generates the master key + Postgres admin password, writes both to KV. ~30 s.
  3. Postgres Flexible Server — slowest single step, ~3-4 min.
  4. App Service Plan + App Service — App Service starts the container image (first pull from ghcr.io, ~1-2 min).
  5. Container Apps Environment + Worker App — ~2 min.

Total: ~5-7 minutes wall clock. Output: the public URL of your Identity Atlas.

First-run post-deploy steps

  1. Open the App URL. First paint takes ~20-30s while the App Service container warms up (subsequent loads are sub-second because Always On is enabled).
  2. Admin → Crawlers → "Load demo data" to explore, or "Add Crawler" to connect Microsoft Graph.
  3. (Optional) Admin → Authentication to switch on Entra ID sign-in. Until then the app is open (so an IP allow-list at the App Service level is the recommended interim).

Operational notes

Updating the container images

# Restart the App Service — re-pulls the :latest tag from ghcr.io
az webapp restart --name <namePrefix>-web --resource-group <rg>

# Restart the worker — same
az containerapp revision restart --name <namePrefix>-worker --resource-group <rg> --revision <latestRevisionName>

To pin to a specific version instead of :latest, redeploy with webImage=ghcr.io/fortigi/identity-atlas:<tag> and the matching workerImage.

Logs

# App Service stdout, live
az webapp log tail --name <namePrefix>-web --resource-group <rg>

# Worker container logs, live
az containerapp logs show --name <namePrefix>-worker --resource-group <rg> --follow

Or query Log Analytics (whether created here or BYO):

AppServiceConsoleLogs | where _ResourceId endswith "<namePrefix>-web"
ContainerAppConsoleLogs_CL | where ContainerAppName_s == "<namePrefix>-worker"

Postgres access

Postgres has a public endpoint with a firewall rule allowing Azure services. To run psql from a workstation: 1. Add a temporary firewall rule for your IP: az postgres flexible-server firewall-rule create --resource-group <rg> --name <pgname> --rule-name temp-yourip --start-ip-address <ip> --end-ip-address <ip>. 2. Fetch the admin password from Key Vault: az keyvault secret show --vault-name <kv> --name postgres-admin-password --query value -o tsv. 3. psql "postgres://identityatlas:<pw>@<pgFqdn>:5432/identity_atlas?sslmode=require". 4. Delete the firewall rule when done.

Tearing down

az group delete --name <rg> --yes

Key Vault has soft delete + purge protection (7-day retention). To redeploy with the same KV name within those 7 days, either wait or use a different namePrefix.

Decisions taken (and why)

Decision Choice Why
Architecture App Service + Postgres Flex + ACA worker Matches the customer's CCoE pattern — no networking provisioning.
Postgres endpoint Public + "Allow Azure services" firewall App Service outbound IPs come from a Microsoft-owned pool; this rule covers them without enumeration.
Key Vault endpoint Public, RBAC-only Access is gated by Key Vault Secrets User role, not network. Anonymous access returns 403.
App Service image source Direct pull from public ghcr.io No ACR needed (~€5/mo saved + simpler deploy). Add ACR later if a tenant demands a private registry.
Worker model ACA App (always-on), not ACA Job "Sync now" should respond in <2 s. Job would have a 60-300 s schedule lag.
Master key + DB password Generated by deployment script, stored in KV, exposed to the app via KV references Zero secrets in the template, the deployment history, or ARM. App reads them via managed identity at startup.
Auth AUTH_ENABLED=false by default Avoids requiring an App Registration before first login. Configurable post-deploy via Admin → Authentication.
HA Off Single-replica everywhere. Keeps cost predictable.
Application Insights Skipped Log Analytics covers stdout + system metrics. AI is for distributed tracing, not needed at this scale.
VNet integration Not in the Simple shape If a customer demands private Postgres / KV later, a separate "Isolated" template will add it with the customer's CCoE-provided subnet IDs.

Limitations

  • No HA / zone-redundancy. Single AZ, single replica. Adding zone redundancy is two Bicep params away — future iteration.
  • No custom domain on App Service. Free *.azurewebsites.net cert covers v1. Custom domain + customer TLS cert is a 2-step manual setup post-deploy.
  • No GitHub Actions deploy workflow. Use Deploy-to-Azure button + deploy.ps1 for now.
  • No Entra App Registration auto-creation. Done manually if/when enabling Entra auth.
  • Public Postgres / KV endpoints. A future "Isolated" template will add private endpoints when a customer wants them.

File index

File Purpose
azure/main.bicep Top-level orchestrator
azure/main.json Compiled ARM (used by the Deploy to Azure button)
azure/main.parameters.example.json Example parameter file
azure/deploy.ps1 CLI deploy + post-deploy summary
azure/modules/log-analytics.bicep New workspace OR BYO lookup OR pass-through
azure/modules/storage.bicep Storage Account + Azure Files share
azure/modules/identities.bicep 2 user-assigned managed identities
azure/modules/key-vault.bicep Key Vault + RBAC (public endpoint)
azure/modules/bootstrap.bicep One-shot deployment script for secrets
azure/modules/postgres.bicep Postgres Flex + firewall
azure/modules/app-service.bicep App Service Plan + App Service for Containers
azure/modules/aca-env.bicep Container Apps Environment (Consumption)
azure/modules/aca-app-worker.bicep Worker Container App (always-on)