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
appFqdn.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:containerImagedefaults 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 frommain.openssl randcreates 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.
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:
- secrets
AZURE_CLIENT_ID(the$APPabove),AZURE_TENANT_ID,AZURE_SUBSCRIPTION_ID; - variables
AZURE_NAME_PREFIX(e.g.nleadbridge) andAZURE_RESOURCE_GROUP.
Security posture
- Two scoped API keys:
EXTERNAL_API_KEY(hand to the customer; forecast ingestion only) andPROCESS_API_KEY(internal pipeline + orchestrator timer)./healthstays 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’stestflag); going live is one switch — store the production key inENGRATE_PRODUCTION_API_KEYand setENGRATE_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 setszentrale_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
2
Build the image and point the app at it
./scripts/rollout_image.sh nleadbrp nleadbrp-rg.3
Let continuous deployment reach the stack
The deploy workflow rolls every green commit on The federated credential trusts the repository’s
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: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 theNLEAD_* 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: