Environment & service-discovery convention
Deployed There is no per-service .env in the cloud.
In staging and prod, configuration lives as GitHub org-level Variables and Secrets, and the
shared deploy engine (service-template)
selects, renames, and injects them by naming convention. Two conventions do all the work:
- Prefix-strip injection — an org variable/secret named
STAGING_<NAME>(orPROD_<NAME>) is injected into the container as<NAME>, with the prefix stripped. - Auto-published
*_GRPCFQDNs — after a service deploys, its address is written back as an org variable, which every other service reads as its connection string on its next deploy.
Get a name wrong and nothing errors — the service quietly falls back to a code default. This page is the contract, so you don’t hit that class of bug.
Prefix-strip env injection
Section titled “Prefix-strip env injection”The deploy job computes a prefix from github_environment (STAGING_ for staging, PROD_ for
prod) and rewrites every matching org variable/secret into an Azure Container Apps (ACA) env flag:
| Org entry | Arrives in the container as | How |
|---|---|---|
Variable STAGING_<NAME> |
<NAME> (plaintext env var) |
prefix stripped |
Secret STAGING_<NAME> |
<NAME>=secretref:<name-kebab> |
value stored as an ACA secret; only a reference appears in the container spec |
So the org variable STAGING_REDIS_ADDR lands as REDIS_ADDR, and the secret
STAGING_INTERNAL_RPC_SECRET lands as INTERNAL_RPC_SECRET via a secretref. Swap the whole set
to a PROD_ prefix and you get the prod configuration — that is the entire “environment” mechanism.
A handful of keys are always ignored so deploy plumbing never leaks into the app: GITHUB_TOKEN,
AZURE_CREDENTIALS, GO_PUBLISH_APP_ID, GO_PUBLISH_APP_PRIVATE_KEY, and GH_READ_REPO_TOKEN.
Service discovery: the publish → inject loop
Section titled “Service discovery: the publish → inject loop”Discovery rides the same string convention. After a service with ingress finishes deploying, the
engine reads its live FQDN and writes it back as an org variable named <ENV>_<SERVICE>_GRPC (the
trailing _STAGING / _PROD is stripped from the service name first). That variable is then
injected — prefix stripped — into every other service on its next deploy.
flowchart LR DEPLOY["deploy core-database-staging"]:::data PUB["publish org variable<br/>STAGING_CORE_DATABASE_GRPC = fqdn:443"]:::external ORG["GitHub org (soundversegit)<br/>Variables + Secrets"]:::external INJECT["next deploy of any consumer<br/>strip prefix → CORE_DATABASE_GRPC"]:::gateway APP["container reads CORE_DATABASE_GRPC"]:::worker DEPLOY --> PUB --> ORG ORG --> INJECT --> APP classDef gateway fill:#0ea5e9,color:#fff,stroke:#0369a1 classDef data fill:#8b5cf6,color:#fff,stroke:#6d28d9 classDef worker fill:#10b981,color:#fff,stroke:#047857 classDef external fill:#f59e0b,color:#111,stroke:#b45309
The published address encodes the transport by port: external apps are published as
<fqdn>:443 (TLS at the ingress), internal apps as <fqdn>:80 (plaintext h2c in-cluster). ACA
has no bare container-name DNS — apps only reach each other by FQDN, and the FQDN differs between
external (<app>.<env-suffix>.<region>.azurecontainerapps.io) and internal (the .internal.
variant). Because the engine re-registers the current FQDN on every deploy, flipping a service
external↔internal updates the address automatically; consumers pick it up on redeploy.
Canonical fleet-wide names
Section titled “Canonical fleet-wide names”These names are read directly by every relevant service, so there is no per-service drift. Names and purpose only — never values.
| Env var name | What it points at |
|---|---|
INTERNAL_RPC_SECRET |
Bearer token on every internal gRPC call (secret; secretref). soundverse-py also accepts the legacy alias INTERNAL_AUTH_SECRET |
ENVIRONMENT |
deployment environment (local / staging / prod); gates fail-fast checks |
REDIS_ADDR |
Redis URL — the logical DB index lives in the URL path (…/0), not a separate knob |
REDIS_PASSWORD |
Redis AUTH for managed Redis, when not embedded in REDIS_ADDR |
CORE_DATABASE_GRPC |
core-database address (auto-published; CORE_DATABASE_URL is an explicit full-URL override) |
CORE_STORAGE_GRPC |
core-storage address |
CORE_IDENTITY_GRPC |
core-identity address |
CORE_MCP_GRPC |
core-mcp address; CORE_MCP_URL is the explicit full-URL override |
CORE_CONVERSION_GRPC |
core-conversion address |
CORE_GATEWAY_CONSUMER_GRPC |
gateway address, read by the saas-2.0 BFF |
<SERVICE>_USE_TLS |
per-service gRPC TLS toggle (see below) |
OTEL_EXPORTER_OTLP_ENDPOINT / OTEL_EXPORTER_OTLP_HEADERS |
telemetry export target |
See the full environment variable catalog and the
Redis key conventions for the {env}:{namespace}: contract.
TLS: external means :443, not plaintext
Section titled “TLS: external means :443, not plaintext”External gRPC apps are reached over TLS on :443 even though the container’s target_port is
80 or 8080 — ACA terminates TLS at the ingress. For example, the consumer gateway deploys with
target_port: 80, is_external: true, but callers dial it on :443. Clients must therefore speak
TLS to any external service; a plaintext h2c dial to :443 is reset by the ingress, surfacing as
gRPC UNAVAILABLE ... recvmsg: Connection reset by peer.
Two mechanisms keep this correct without hand-editing URLs on every flip:
-
Scheme-follows-port derivation. Both languages pick the transport from the published port:
:80→http/h2c (internal plaintext ingress), any other port →https/TLS. In Python,ServiceRegistryEnv._derive_core_mcp_urlbuilds{http|https}://<host:port>/mcpfromCORE_MCP_GRPC(seesoundverse-py/src/soundverse/config.py). In Go,resolveServiceURLdoes the same forCORE_DATABASE_GRPC/CORE_STORAGE_GRPC(seecore-mcp/internal/config/config.go), and the matching client transport is chosen incore-mcp/internal/clients/clients.go. -
Per-service TLS toggles. The db/storage/conversion channels also carry explicit boolean flags —
CORE_DATABASE_USE_TLS,CORE_STORAGE_USE_TLS,CORE_CONVERSION_USE_TLS— that default totrue(secure by design). These are declared separately in the Python services (soundverse-py, core-identity, core-gateway-consumer, core-storage), not auto-derived from the port.
Adding a new env var end-to-end
Section titled “Adding a new env var end-to-end”-
Pick the canonical name your code reads (e.g.
CORE_WIDGET_GRPC). Cross-check it against any existing name for the same thing — the string must match exactly on both sides. -
Create the org entry, prefixed per environment:
STAGING_CORE_WIDGET_GRPCas an org Variable, orSTAGING_<NAME>as an org Secret for anything sensitive. -
Redeploy the consuming service by pushing to its
staging(orprod) branch. The engine strips the prefix and injectsCORE_WIDGET_GRPCon that deploy — existing containers do not pick up a new org variable until they redeploy. -
Verify the container actually received it — read the live env with
az containerapp show. Secret-injected vars show as asecretRef, not the value, by design.
Related
Section titled “Related”- CI/CD deploy pipeline — the branch-push → shared-template flow this rides on
- Add a new service to the pipeline — wiring a repo in (ingress vs no-ingress worker)
- Inspect a running service’s env & secrets — the
azrunbook - Configuring .env — the same names locally, with TLS off
- Environment variable catalog — every fleet env-var name
- core-mcp — the streamable-HTTP endpoint derived from
CORE_MCP_GRPC