Skip to content

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:

  1. Prefix-strip injection — an org variable/secret named STAGING_<NAME> (or PROD_<NAME>) is injected into the container as <NAME>, with the prefix stripped.
  2. Auto-published *_GRPC FQDNs — 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.

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.

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.

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: :80http/h2c (internal plaintext ingress), any other port → https/TLS. In Python, ServiceRegistryEnv._derive_core_mcp_url builds {http|https}://<host:port>/mcp from CORE_MCP_GRPC (see soundverse-py/src/soundverse/config.py). In Go, resolveServiceURL does the same for CORE_DATABASE_GRPC / CORE_STORAGE_GRPC (see core-mcp/internal/config/config.go), and the matching client transport is chosen in core-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 to true (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.

  1. 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.

  2. Create the org entry, prefixed per environment: STAGING_CORE_WIDGET_GRPC as an org Variable, or STAGING_<NAME> as an org Secret for anything sensitive.

  3. Redeploy the consuming service by pushing to its staging (or prod) branch. The engine strips the prefix and injects CORE_WIDGET_GRPC on that deploy — existing containers do not pick up a new org variable until they redeploy.

  4. Verify the container actually received it — read the live env with az containerapp show. Secret-injected vars show as a secretRef, not the value, by design.