2026-05-14 — Cost-minimised Container Apps topology: one app, four sidecars¶
Motivation¶
The 2026-05-14 Celery + self-hosted Redis plan still provisioned three Container
Apps (ca-control-api, ca-control-worker, ca-control-beat) plus a dedicated
Redis VM. The user asked whether everything could be collapsed into a single
Container App to minimise cost.
Container Apps supports multiple containers per revision (sidecar pattern),
sharing one network namespace and lifecycle. For this low-traffic, single-tenant
control plane, a single Container App with api, worker, beat, and redis
sidecars is operationally simpler and substantially cheaper than four separate
billable units.
User-facing change¶
None at runtime (planning document update).
Architecture diff summary¶
| Area | Previous (multi-app) | Now (bundled sidecars) |
|---|---|---|
| Container Apps | ca-control-api, ca-control-worker, ca-control-beat (3 apps) |
ca-elb-control (1 app) with four containers: api, worker, beat, redis |
| Redis broker | Self-hosted on vm-elb-redis (Linux VM, Standard_B2s) |
redis:7-alpine sidecar in the same revision, listening on 127.0.0.1:6379 |
| Redis persistence | Local AOF + nightly backup blob from VM | AOF written to an Azure Files share redis-data mounted at /data |
| Subnets | snet-containerapps + dedicated snet-redis for the broker VM |
snet-containerapps only |
| NSG for Redis | NSG locked to ingress from snet-containerapps |
None — Redis is loopback-bound inside the replica |
| Identities | id-elb-control-api, id-elb-control-worker, id-elb-redis |
One shared id-elb-control for all four sidecars |
| Replica scaling | Per-app, with worker scalable |
Whole app pinned to minReplicas: 1, maxReplicas: 1 (forced by beat singleton + Redis state locality) |
| Private DNS zones | vault, blob, table, acr | + privatelink.file.core.windows.net (needed for the Redis AOF mount once Storage public access is disabled) |
| Bicep modules | redisVm.bicep, separate Container Apps module |
containerAppControl.bicep (single app + sidecars + volume mount); redisVm.bicep removed |
Trade-offs (now explicit in the doc)¶
- No horizontal scale-out. The whole stack is one replica because beat is
a singleton and Redis state must stay co-located. The doc records the
escalation path: split
beat+redisinto a separate app first if scale- out is ever needed, then moveworker. - Whole-stack restart on any image change. Acceptable because the API surface is small. Beat's reconciler re-dispatches in-flight tasks after restart.
- In-flight task loss on restart is bounded by AOF on Azure Files plus the
reconciler that watches Storage state for
runningrows whose worker disappeared. - Shared MI over-grants the API sidecar. Mutating ARM operations only run inside Celery task handlers; documented as an explicit compromise. Future split into per-sidecar Container Apps would restore per-process identities.
Files changed¶
docs/container-apps-migration.md— Decision Summary, Resources to Create, Target Architecture diagram, Component Plan, Service Boundaries (collapsed three sections into four sidecar sections), Route Migration Map, Networking Plan (removedsnet-redis, addedprivatelink.file.core.windows.net), Identity table (single shared identity), Storage Plan (addedredis-dataAzure Files share), Infrastructure Changes (Bicep modules), Phases 2 and 4, Validation Plan, Cutover Checklist, Rollback, Risks, Open Decisions (addedTopologyrow), First Implementation Slice.README.md— Architecture Planning bullet rewritten for the bundled topology.
Validation evidence¶
Documentation-only change. Verified with:
All remaining matches are inside the "Explicitly removed from the prior plan" table — no active recommendation references the deleted resources.
Resource count delta¶
Removed (vs the 2026-05-14 multi-app plan):
- 1 × Linux VM (
vm-elb-redis) including OS disk, NIC, and public IP if any - 1 × subnet (
snet-redis) and its NSG - 1 × user-assigned MI (
id-elb-redis) - 1 × Key Vault secret for the Redis password (no longer needed; loopback)
- 1 × nightly backup orchestration target (Redis VM AOF copy)
- 2 × Container App revisions (3 apps → 1 app)
Added:
- 1 × Azure Files share
redis-dataon the platform Storage account (no new account required) - 1 × additional private DNS zone
privatelink.file.core.windows.net