Skip to content

Service ↔ Azure Resource Model

This page is the in-repo source of truth for how a Forge service (a logical group of per-environment Entra ID application registrations) maps to the live Azure resources that run it. It exists so that any CLI feature reading deployed or runtime state — service describe image tags, logs, health, cost, restart, config-drift, and so on — can be built against documented, verified facts instead of reverse-engineered guesses.

Read this before building an Azure-touching feature

Several of the facts below are not derivable from this repository alone (per-environment subscription IDs and the CLI's Azure scopes live in cloud-foundations). They were verified against live Azure with az / Azure Resource Graph. The closing Authoring Azure-touching features checklist captures what a new feature or issue must state up front.


🧭 The big picture

flowchart TD
    R["Repository<br/>(exact-name lookup)"]
    S["Service<br/>(common key)"]
    subgraph envs["Per environment"]
        A["Entra app registration<br/>displayName = {projectId}-{env}"]
        W["Azure App Service<br/>name = displayName (1:1)"]
        SL["0..N deployment slots<br/>(e.g. pr-10028)"]
    end
    R -. correlated .-> S
    S -->|"one facet per env<br/>(test, qa, uat, prod, ...)"| A
    A -->|"identical name"| W
    W --> SL
    W -. "lives in" .-> SUB["Subscription<br/>(one per environment)"]

A service is not a single Azure resource. It is a key (payments) under which one application registration — and one App Service — exists per environment, and each environment lives in a different subscription.


🏷️ The naming invariant (1:1)

The Azure App Service name is byte-identical to the Entra application-registration display name. There is no transformation, abbreviation, or reconstruction step between the two.

Plane Value (example)
Entra app-registration displayName pol-api-exp-policyportal-test
Azure App Service name pol-api-exp-policyportal-test

The display name follows the platform project-id convention plus an environment suffix:

{domain}-{app type}[-{app sub type}]-{app name}-{env}
└────────────── projectId ──────────────┘ └─ env ─┘
  • {domain}-{app type}[-{app sub type}]-{app name} is the projectId (e.g. pol-api-exp-policyportal). See saif service describe → repository correlation for the same {domain}-{app type}{-app sub type}-{app name} shape.
  • {env} is a recognized environment suffix (see below).

Do not reconstruct or short-form the name

The name is identical across environments apart from the suffix — verified byte-for-byte across prod, qa, test, and uat. Do not short-form the environment (e.g. prodprd) and do not rebuild the name from projectId + env with custom casing. Take the app-registration displayName and use it verbatim as the App Service name. Building it any other way is the exact mistake that produced an incorrect implementation sketch in earlier work.

Environment suffixes

Only names ending in a known platform environment suffix are service facets. The authoritative list lives in ServiceGrouper (SAIF.Platform.Sdk.Catalog.ServiceGrouper) and is matched longest-suffix-first:

platformdev, prod, test, uat, qa

A registration without one of these suffixes (a free-text or vendor app registration) is not a platform service and is excluded from grouping rather than mapped to a synthetic environment.


🌍 Environment → subscription topology

Each environment of a service runs in a different Azure subscription. For a single service, expect a distinct subscription GUID per environment (verified: four distinct subscriptions for prod / qa / test / uat).

There is no in-repo environment → subscription map

The per-environment subscription IDs are owned by cloud-foundations, not this repository. Do not hardcode a subscription map and do not assume a discoverable env → subscription → resource group mapping exists here — it does not.

Discovery strategy: Azure Resource Graph, by exact name

Because the naming invariant guarantees a globally unique, exact App Service name, the supported way to locate a service's resources is an Azure Resource Graph (ARG) query by exact name across all subscriptions the caller can see — never a hardcoded subscription/resource-group map.

Resources
| where type =~ 'microsoft.web/sites'
| where name =~ 'pol-api-exp-policyportal-test'
| project name, resourceGroup, subscriptionId, location, tags, properties.siteConfig.linuxFxVersion

This automatically spans the per-environment subscriptions without the CLI knowing which subscription an environment maps to, and it degrades gracefully: a subscription the caller cannot read simply returns no row instead of failing the whole query.


🗂️ The catalog data model (lossy by design)

The service catalog is built in two stages, and the second stage discards information the first stage read. Knowing this prevents you from expecting the App Service name to be available downstream when it is not.

Stage Type Keeps Discards
Read EntraServiceCatalogProvider Full app-registration displayName, appId, identifierUris
Group ServiceGrouper.Group Common key, environment suffix, AppId, identifier URI (per env) The full display name / base name detail

The consequence: after grouping, the catalog knows a service's key and per-environment AppId, but not the literal App Service name. A feature that needs the App Service name must either reconstruct it from the catalog key + environment suffix (which reproduces the naming invariant exactly), or re-read the source app registration. Prefer re-reading the registration displayName when accuracy matters — it is the invariant's authoritative side.


🔖 The resource tag contract

Deployed-version information is read from the App Service resource tags and site config, which the deploy pipeline sets through the Terraform service modules.

build_number

The Forge service modules set a build_number tag from the deploying pipeline's build number via their common_tags:

# src/templates/saif-feature-api/infra/api/app.generated.tf (and web equivalent)
locals {
  common_tags = {
    deployed_by      = var.deployed_by
    application_name = local.variables.application_name
    build_number     = var.build_number   # ← snake_case here
    team_name        = local.variables.owner
  }
}
Owner module Registry source Sets tag as
API services app.terraform.io/SAIFCorp/saif-apiservice/azure build_number
Web apps app.terraform.io/SAIFCorp/saif-webapp/azure build_number

Casing and presence vary by plane

  • Casing is not guaranteed. The templated modules above emit build_number (snake_case), but other planes / older deployments may carry BuildNumber (PascalCase). Read the tag case-insensitively.
  • Slots carry no build tag. A deployment slot does not get the build_number tag — only the main site does. Read a slot's version from its image (linuxFxVersion) instead.

linuxFxVersion (the deployed image)

The deployed container image is read from the site config, not a tag:

properties.siteConfig.linuxFxVersion = "DOCKER|{registry}/{repo}:{tag}"

Parse the :{tag} portion for the image tag. This is the most reliable per-plane (site and slot) source of "what is actually running", because every slot has its own linuxFxVersion even when it has no build_number tag.


🧩 Deployment slots

App Service deployment slots are zero-or-more and arbitrarily named — not a single fixed "staging" slot.

  • A service may have no slots, or several.
  • Slot names are feature-driven. PR-preview slots are named pr-{number} (e.g. pr-10028); see PR slot deployments.
  • Each slot has its own image (linuxFxVersion) and therefore its own deployed version.
  • Slots do not inherit the main site's build_number tag.

A feature that reports deployed state must enumerate slots dynamically and read each slot's linuxFxVersion independently, rather than assuming one well-known slot.


🔐 Azure-read prerequisite (not in place today)

The CLI has no Azure Resource Manager awareness today, and the CLI's own application registration lacks the scope required to read ARM/ARG on the user's behalf:

  • Required: the delegated Azure Service Management user_impersonation scope (https://management.azure.com/user_impersonation) on the CLI app registration, plus tenant admin consent.
  • This is an infrastructure change in cloud-foundations, not a code change in this repo.

Distinct from a service's own API scope

This Azure Service Management user_impersonation is not the same as the per-service api://{projectId}-{env}/user_impersonation scope described in How Authentication Works. The former authorizes calls to Azure Resource Manager; the latter authorizes calls to a Forge API. A feature needing live Azure reads depends on the former and must treat it as a prerequisite, not an assumption.


✅ Authoring Azure-touching features

Any Forge feature or CLI command that reads or operates on live Azure resources should state, up front, how it handles each of the following — and verify the assumptions against live Azure (az / Azure Resource Graph) before finalizing:

  • Naming invariant(s) it relies on — and that it uses the app-registration displayName verbatim as the App Service name (no short-forming, no reconstruction with custom casing).
  • Discovery strategy — default to ARG by exact name across visible subscriptions; do not hardcode an env → subscription → resource group map.
  • Required Azure scopes / app-registration changes and where they live — e.g. the Azure Service Management user_impersonation scope and per-environment subscription IDs are owned by cloud-foundations, not this repo.
  • Tag contract assumptions — read build_number case-insensitively, expect it to be absent on slots, and prefer linuxFxVersion for per-plane image/version.
  • Slot handling — enumerate zero-or-more arbitrarily named slots; never assume a single staging slot.
  • Best-effort degradation — a failure for one environment/subscription (no access, no resource, transient error) must not sink the whole view; surface per-environment status and keep going.

Topic Link
saif service search / describe SAIF CLI reference
Token acquisition and scopes How Authentication Works
Environment flow and approvers Environments
PR-preview deployment slots PR Slot Deployments