Skip to main content
The service runs as a single Azure Container App plus a scheduled job. The entire footprint is one Bicep file — infra/main.bicep:
Why one warm replica? Traffic is a handful of calls per day plus the orchestrator ping, but the calls that matter are the customer’s — a forecast POST arriving on a scaled-to-zero app pays a cold start, and one arriving in the scale-in drain window gets a 503. minReplicas: 1 removes both; max one replica keeps the file-based state single-writer.

Bootstrap

The ACR doesn’t exist until the template runs, so the first deployment uses a placeholder image and then swaps in the real one:
1

Deploy the infrastructure

2

Build the image in ACR (no local Docker needed)

3

Point the app at the real image

The public URL is in the deployment output appFqdn.
Code redeploys only need steps 2–3, and continuous deployment does them for every green commit on main.

Re-running the template

Changes to the template itself (a new resource, an identity, a setting) reach a running stack only through a new run of step 1. Continuous deployment only swaps the image. A run sets the app to exactly what it is given, so step 1 as written is for the first run only:
  • containerImage defaults to the hello-world placeholder. A run without it creates a revision with the placeholder, which replaces the bridge as soon as it is ready and stays until the next rollout from main.
  • openssl rand creates new inbound keys: the console key and the customer’s key stop working.
  • A secret left out is cleared on the app. Secrets and variables set outside the template are dropped as well, for example the mobile device keys. Set them again after the run.
Against a running stack, pass the image the app runs and the values it runs with: the keys from the password manager, or from az containerapp secret list -n "${PREFIX}-app" -g "$RG" --show-values.

Continuous deployment

deploy.yml rolls out every commit on main once CI has passed on it. It runs on the completion of the CI workflow, not on the push itself: a red main is never deployed, and the image is built from exactly the commit CI tested. A manual run (Run workflow) deploys the head of main, and only when a CI run of a push has passed on exactly that commit; started from any other branch it stops before building anything (Azure trusts main only). A commit that changes nothing outside mobile/, docs/ and .github/workflows/mobile.yml since the running revision is not rolled out: the image would be identical, and every rollout restarts the service. The comparison is against the image tag the container app runs, so a push of several commits or a rollout that failed earlier still deploys. When the running tag cannot be found in the history the commit is deployed; a manual run always deploys, under the conditions above. Each run builds the image in ACR, tagged with the short commit hash, checks that the tag exists before touching the app, rolls the container app onto it and waits until the new revision is ready and /health answers. Rollouts are serialised; a newer commit waits for the running one. Azure trusts the workflow through OIDC — no password is stored. One-time setup, with an az login that may assign roles:
Then, under Settings → Secrets and variables → Actions:
  • secrets AZURE_CLIENT_ID (the $APP above), AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID;
  • variables AZURE_NAME_PREFIX (e.g. nleadbridge) and AZURE_RESOURCE_GROUP.
Until those exist the workflow skips with a warning instead of failing. The roles are scoped to the registry and the container app, not the resource group; ACR admin stays off.

Security posture

  • Two scoped API keys: EXTERNAL_API_KEY (hand to the customer; forecast ingestion only) and PROCESS_API_KEY (internal pipeline + orchestrator timer). /health stays open for probes.
  • Secrets live as Container Apps secrets — move to Key Vault references when the team standardizes on it.
  • Before live Engrate submissions, set the real balancing-group EICs (AREA_MARKET_IDS) and the ECC counterparty EIC. The bridge starts against Engrate staging (ENGRATE_ENVIRONMENT=staging, submissions carry Engrate’s test flag); going live is one switch — store the production key in ENGRATE_PRODUCTION_API_KEY and set ENGRATE_ENVIRONMENT=production (also possible at runtime from the console’s Engrate tab).
  • If the customer calls from fixed egress IPs, add ingress IP restrictions on the Container App.

A stack per market party

The bridge serves one balance responsible party (BKV) per running instance: its MP-ID, balance groups, mailbox, PARTIN datasheet, trader file and audit trail are one configuration. A second BKV gets a second stack from the same template and the same image, with its own resource group, registry, audit share and keys. The two never share state; they share only the mako365 account, whose reads each instance filters to its own MP-ID. The market data that are the same for every Mandant (the reBAP from netztransparenz.de and ENTSO-E, the partners’ PARTIN datasheets) are read once, by the stack that is the Zentrale, and copied by the others over its report key. A new stack sets zentrale_url and zentrale_api_key in the console instead of its own netztransparenz and ENTSO-E credentials, and the Zentrale adds its MP-IDs to zentrale_mandanten. The Zentrale is a small stack of its own (nleadzentrale, infra/zentrale.bicepparam): no BKV, scaled to zero, steps in Zentrale. The code’s defaults are the customer’s identity. A stack for another party therefore sets its own through the template’s market-party parameters (bkvMpId, bkvEic, lieferantMpId, areaMarketIds, partinCompany): each one reaches the container only when given, so redeploying an existing stack without them changes nothing. marketRoutinesOff=true seeds the routines that act on their own (APERAK answering, mailbox reconciliation) as switched off on a fresh audit share; from then on the Agenten tab is the switch. lrvRoutineOn=true seeds the Lieferantenrahmenvertrag routine (agent lrv) as switched on, under the same rule.

n:lead’s own BKV (MP-ID 9984303000001)

infra/nlead.bicepparam holds the parameters: prefix nleadbrp, n:lead’s MP-IDs as BKV and as Lieferant (9984302000002) and its EIC, no test sFTP, n:lead’s own company data in place of the customer’s (from its Kontaktdatenblätter), market routines off, the LRV routine on. It refuses to build without the two inbound keys. The Lieferant’s mailbox (LF_SMTP_*, LF_IMAP_*) is set in the console, group Lieferant. The LRV routine does nothing until that mailbox is set. The stack runs in Switzerland North: every resource takes the resource group’s region. A resource cannot move region afterwards; moving a stack means deleting its resource group and deploying it again in the new one.
1

Deploy the infrastructure

Keep the two generated keys in the password manager: the process key opens the console, the external key goes to whoever delivers the forecasts. This command is for the first run only; later runs are described below the steps.
2

Build the image and point the app at it

Or in one go, with every step checked and the revision’s logs printed if it does not come up: ./scripts/rollout_image.sh nleadbrp nleadbrp-rg.
3

Let continuous deployment reach the stack

The deploy workflow rolls every green commit on main out to each stack whose variables are set. Add the repository variables AZURE_NAME_PREFIX_NLEAD=nleadbrp and AZURE_RESOURCE_GROUP_NLEAD=nleadbrp-rg, and give the existing deploy identity the same two roles on the new stack:
The federated credential trusts the repository’s main, not a stack, so it needs no change.
4

Configure the party in the console

Open https://<appFqdn>/console with the process key. Before anything reaches the market, in the Markt group: area_market_ids (n:lead’s balance group per control area), lieferant_mp_id, partin_company; home_control_area comes as Amprion from the parameters. Then the trader sFTP, the Erstkontakt mailbox and alerting. The MakoFlow login comes last: without it the instance cannot send a market message.
5

Read in the activations of earlier years

The new stack sees 180 days of inbox; TenneT, TransnetBW and Amprion activated n:lead’s series in 2024/25. Once the MakoFlow login is set, read them into the ledger (details):
6

Decide who answers n:lead's inbound messages

Engrate market access may already acknowledge messages addressed to 9984303000001. Switch on APERAK answering in the Agenten tab only once this bridge is the one that answers, or the partners get two APERAKs for one message.

Re-running the template on this stack

The same rules apply as for any re-run. Export the NLEAD_* values of the first run from the password manager. Do not generate new keys with openssl rand: a new value rotates the console key and the inbound key. A value left unset clears its secret on the app (NLEAD_SFTP_PASSWORD too, if the first run set it). Pass the running image as an override after the parameter file: