Skip to content

Onboarding a Customer Environment

This runbook is for the platform team. It describes how a new environment of the portal is added to the shared code base and deployed. It is generic: values appear as placeholders (<customer>, <env>, <tenant-id>) and the procedure describes the reference design. Your deployment may differ. "Customer" means the organisation that owns the Azure subscriptions; when the platform team and the customer are the same organisation (self-managed model), read the steps accordingly (Operating models).

Code and pipelines stay with the provider

In the delivery model of the source, the customer gets a deployed product and the Azure resources in its subscription. The customer does not receive the code or the pipelines. How a self-managed team runs the portal without them is not described in the source, so agree it with the provider first.

Phases

Phase Result Owner
Plan Kick-off held (60 minutes): what the portal is, key messages, what the customer provides, timeline. Questionnaire and prerequisites sent afterwards. Delivery manager
Prepare Questionnaire completed and prerequisites fulfilled Engineer tracks, customer fulfils
Deploy Environment defined in the repository and deployed through the pipeline. Nothing is configured by hand in the customer tenant, except the approvals and grants below. Engineer
Validate A second engineer checks the validation list Second engineer
Closure Technical hand-over meeting (60 to 90 minutes) with the customer's technical and security contacts: demo with the customer's own data, self-service, agents, MCP in the development environment. Acceptance. Delivery manager, engineer demos

Key messages to repeat

Azure spend lands on the customer's bill. The provider keeps ownership of the code. Data stays in the customer environment. State the operational guarantee as agreed in the contract: it is not described in the technical sources.

Customer-specific code changes are not made. A request that does not fit the shared code base becomes separate feature work. See ADR-0007 in Decisions.

Procedure

Use a branch and a pull request for steps 1 to 6.

1. Create the GitHub Environments

Create one GitHub Environment per Azure environment, named <customer>_<env> (for example <customer>_acc and <customer>_prod). Require reviewers on production. Acceptance has no reviewers. The environment holds the approval gate for applying a Terraform plan (Deployment and environments).

2. Add federated credentials

Add Entra federated credentials on the deployment app registration, one for each of these subjects, with audience api://AzureADTokenExchange:

Subject Used by
pull_request Plan on pull requests
ref:refs/heads/main Plan and build on main
environment:<customer>_<env> Apply, one per environment

The issuer is the OIDC issuer of the GitHub Enterprise tenant. The subjects use the immutable owner and repository IDs rather than names. If no federated credential can be created, fall back to a client secret (AZURE_CLIENT_SECRET_<OWNER>_<ENV>) and ask again for the federated credential. See Authentication to Azure.

3. Add rows to the environments file

Add one row per environment to the environments file (infra/customer-environments.json). The row carries the owner, environment, ring, Terraform backend, deployment identity and the web app and registry names. See The environments file.

Put the first environment (acceptance or test) in the same ring as the first environments of other customers and production in the next ring. A strict order relative to a specific other customer needs a new ring number, which changes the order for every customer. Discuss that before you do it.

4. Set the secrets

Create the repository secret that the row refers to for the Terraform client secret, BACKSTAGE_CLIENT_SECRET_<OWNER>_<ENV>. It is the only real secret in the set-up; the pull request carries only its name. Other app secrets (for example GitHub App values) are named in the row and surface as Key Vault references.

5. Add the infrastructure

Each customer environment has its own Terraform root module:

infra/
  <customer>/
    acc/main.tf
    prod/main.tf

The root composes the shared modules: spoke canvas (read existing network), naming, resource group, App Service plan, Key Vault, container registry, Azure Monitor, tenant configuration, the backstage module and, if needed, foundry_platform, cost_insights and pagerduty. See Deployment and environments and ADR-0009 in Decisions.

Set the app setting APP_CONFIG_organization_name on the backstage module. It selects the configuration file app-config.<customer>.yaml, so the value in the module and the file name must match exactly, including case.

6. Add the configuration file

Create app-config.<customer>.yaml with the Microsoft sign-in and the repository provider. Example (placeholders):

auth:
  providers:
    microsoft:
      production:
        clientId: ${BACKSTAGE_CLIENT_ID}
        clientSecret: ${BACKSTAGE_CLIENT_SECRET}
        tenantId: ${BACKSTAGE_TENANT_ID}
        signIn:
          resolvers:
            - resolver: emailMatchingUserEntityAnnotation

catalog:
  providers:
    azureDevOps:
      <organisation>:
        host: dev.azure.com
        organization: <organisation>
        project: '*'
        repository: '*'
        path: 'catalog-info.yaml'
        schedule:
          frequency: { minutes: 30 }
          timeout: { minutes: 3 }

Optionally add a feature loader for the customer in packages/backend/src/features/<customer>.ts to turn backend plugins on or off. Do this only when a configuration flag cannot do it. See Configuration.

Optionally add alerting. The pagerduty module creates a service, a dependency on a business service and an Azure Monitor action group. The shared escalation policy that it refers to must already exist (Operations).

Private connectivity

Several rounds are needed

When the provider deploys from its own network, it needs private endpoints to reach resources that it creates itself. The provider needs a connection to deploy, and the deployment creates the next resources it must reach. Expect a first run to fail.

  1. Add the Terraform state storage account to the list of private connections in the provider's platform root (resource ID and subresource blob).
  2. Merge. The platform pipeline plans on pull requests and applies on merge to main.
  3. The customer approves the pending private endpoint connections on the target resources. The request carries a message that explains it is needed for the deployment. The customer can revoke a connection later.
  4. Connect the new private endpoints to the private DNS zones of the platform. This step is manual for endpoints that need approval, because the policy that does it automatically does not apply to them.
  5. Run the dispatch workflow with infrastructure on and the portal build off:

    gh workflow run cicd-dispatch.yml --ref <your-branch> \
      -f "deploy_to=ring 3" -f deploy_infra=true -f build_and_deploy_backstage=false
    

    The first run fails when Terraform reaches Key Vault or the registry over the data plane, because their private endpoints do not exist yet. This is expected. 6. Add the private connections for the Key Vault (vault), the container registry (registry) and the other resources that now exist (SQL server, web app and staging slot, blob). Repeat steps 2 to 5 until the run succeeds.

Approving connections can be scripted. The core command is:

az network private-endpoint-connection approve --id <connection-id> \
  --description "Required for the deployment of the portal"

7. Deploy the portal

Run the dispatch workflow with both infrastructure and portal on:

gh workflow run cicd-dispatch.yml --ref <your-branch> \
  -f "deploy_to=ring 3" -f deploy_infra=true -f build_and_deploy_backstage=true

Then repeat steps 5 and 6 for the next environment and deploy it.

8. Grant access to the managed identity

The portal's managed identity exists only after the first deployment. Then:

  1. An Entra administrator grants the Graph application permissions User.Read.All and GroupMember.Read.All with admin consent (Prerequisites).
  2. The cloud platform administrator grants the Azure roles (Prerequisites).

Until both are done, users and groups are missing and the insights are empty.

9. Enable optional features

Feature Page
Cost insights Cost insights setup
AI agents AI setup
Custom domain and WAF WAF and custom domain
GitHub integration, theme, module registry Configuration

Validation

A second engineer performs the checks and records the entity or value used for each, not only a tick.

Check How
Sign-in A user of the configured group signs in and lands on a populated catalog
Azure ingestion A known workload appears in the catalog, with insights (cost, compliance, security)
Cost timing Exports normally run within 24 hours and ingestion overnight, so cost data can take one to two days. Trigger them manually if needed. A known subscription shows the last full month.
Repository ingestion A known repository's catalog-info.yaml appears
Users and groups A known user and group exist
Agents The spoke agent answers a test prompt such as "List all spoke files", and the tool list of the MCP server is not empty
Access control A user outside the owning team cannot see the firewall blocked-request detail of a workload they do not own
Private endpoints Key Vault, registry and SQL server are reached only over private endpoints
Data residency No data leaves the customer's subscriptions
Security baseline Defender recommendations triaged, no public network access on data services, managed identities used everywhere