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.
- Add the Terraform state storage account to the list of private connections in the provider's platform root (resource ID and subresource
blob). - Merge. The platform pipeline plans on pull requests and applies on merge to main.
- 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.
- 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.
-
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=falseThe 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:
- An Entra administrator grants the Graph application permissions
User.Read.AllandGroupMember.Read.Allwith admin consent (Prerequisites). - 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 |